@corenel/sidecar 0.8.7 → 0.9.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/cli.js +2015 -1085
- package/package.json +10 -8
package/dist/cli.js
CHANGED
|
@@ -401,7 +401,7 @@ var init_protocol = __esm({
|
|
|
401
401
|
|
|
402
402
|
// ../tools-node/fileService.ts
|
|
403
403
|
import { promises as fs3 } from "node:fs";
|
|
404
|
-
import { join as
|
|
404
|
+
import { join as join4, resolve, relative, dirname as dirname2, sep, basename, isAbsolute } from "node:path";
|
|
405
405
|
async function looksLikeGeneratedCoverage(absDir) {
|
|
406
406
|
try {
|
|
407
407
|
const names = await fs3.readdir(absDir);
|
|
@@ -484,7 +484,7 @@ var init_fileService = __esm({
|
|
|
484
484
|
return;
|
|
485
485
|
} catch (e2) {
|
|
486
486
|
if (e2 instanceof Error && e2.message.includes("via symlink")) throw e2;
|
|
487
|
-
const parent =
|
|
487
|
+
const parent = dirname2(probe);
|
|
488
488
|
if (parent === probe) throw new Error(`cannot resolve path under the workspace root: ${a2}`);
|
|
489
489
|
probe = parent;
|
|
490
490
|
}
|
|
@@ -507,7 +507,7 @@ var init_fileService = __esm({
|
|
|
507
507
|
const out = [];
|
|
508
508
|
for (const e2 of entries.sort((a2, b2) => a2.name.localeCompare(b2.name))) {
|
|
509
509
|
if (SKIP.has(e2.name)) continue;
|
|
510
|
-
const absChild =
|
|
510
|
+
const absChild = join4(absDir, e2.name);
|
|
511
511
|
const path = normalizeFilePath(relative(this.root, absChild));
|
|
512
512
|
if (e2.isDirectory() && SKIP_PATHS.has(path)) continue;
|
|
513
513
|
if (e2.isDirectory() && e2.name === "coverage" && await looksLikeGeneratedCoverage(absChild)) continue;
|
|
@@ -535,19 +535,19 @@ var init_fileService = __esm({
|
|
|
535
535
|
}
|
|
536
536
|
async write(path, content) {
|
|
537
537
|
const a2 = this.abs(path);
|
|
538
|
-
await fs3.mkdir(
|
|
538
|
+
await fs3.mkdir(dirname2(a2), { recursive: true });
|
|
539
539
|
await this.assertReal(a2);
|
|
540
540
|
await fs3.writeFile(a2, content, "utf8");
|
|
541
541
|
}
|
|
542
542
|
async writeBytes(path, bytes) {
|
|
543
543
|
const a2 = this.abs(path);
|
|
544
|
-
await fs3.mkdir(
|
|
544
|
+
await fs3.mkdir(dirname2(a2), { recursive: true });
|
|
545
545
|
await this.assertReal(a2);
|
|
546
546
|
await fs3.writeFile(a2, bytes);
|
|
547
547
|
}
|
|
548
548
|
async create(path, content = "") {
|
|
549
549
|
const a2 = this.abs(path);
|
|
550
|
-
await fs3.mkdir(
|
|
550
|
+
await fs3.mkdir(dirname2(a2), { recursive: true });
|
|
551
551
|
await this.assertReal(a2);
|
|
552
552
|
try {
|
|
553
553
|
await fs3.writeFile(a2, content, { flag: "wx" });
|
|
@@ -561,7 +561,7 @@ var init_fileService = __esm({
|
|
|
561
561
|
}
|
|
562
562
|
async rename(from, to) {
|
|
563
563
|
const t2 = this.abs(to);
|
|
564
|
-
await fs3.mkdir(
|
|
564
|
+
await fs3.mkdir(dirname2(t2), { recursive: true });
|
|
565
565
|
await this.assertReal(t2);
|
|
566
566
|
await fs3.rename(await this.realAbs(from), t2);
|
|
567
567
|
}
|
|
@@ -594,21 +594,24 @@ var init_fileService = __esm({
|
|
|
594
594
|
// ../tools-node/auth.ts
|
|
595
595
|
import { promises as fs4 } from "node:fs";
|
|
596
596
|
import { homedir as homedir2 } from "node:os";
|
|
597
|
-
import { join as
|
|
597
|
+
import { join as join5 } from "node:path";
|
|
598
598
|
function corenelDir2(base = homedir2()) {
|
|
599
|
-
return
|
|
599
|
+
return join5(base, stateDirName());
|
|
600
600
|
}
|
|
601
601
|
function sidecarDir(base = homedir2()) {
|
|
602
|
-
return
|
|
602
|
+
return join5(corenelDir2(base), "sidecar");
|
|
603
|
+
}
|
|
604
|
+
function authTokenPath(base = homedir2()) {
|
|
605
|
+
return join5(corenelDir2(base), "token");
|
|
603
606
|
}
|
|
604
607
|
function credentialDir(stateDir) {
|
|
605
608
|
return stateDir ?? corenelDir2();
|
|
606
609
|
}
|
|
607
610
|
function tokenFilePath(stateDir) {
|
|
608
|
-
return
|
|
611
|
+
return join5(credentialDir(stateDir), "token");
|
|
609
612
|
}
|
|
610
613
|
function credentialFilePath(stateDir) {
|
|
611
|
-
return
|
|
614
|
+
return join5(credentialDir(stateDir), "auth.json");
|
|
612
615
|
}
|
|
613
616
|
async function writeFilePrivateAtomic(path, contents) {
|
|
614
617
|
const tmp = `${path}.tmp-${process.pid.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -636,6 +639,49 @@ async function writeAuthToken(token, stateDir) {
|
|
|
636
639
|
} catch {
|
|
637
640
|
}
|
|
638
641
|
}
|
|
642
|
+
async function writeLoginCredential(token, opts = {}) {
|
|
643
|
+
const cred = {
|
|
644
|
+
accessToken: token.trim(),
|
|
645
|
+
...opts.account ? { account: opts.account } : {},
|
|
646
|
+
...opts.apiBase ? { apiBase: opts.apiBase } : {}
|
|
647
|
+
};
|
|
648
|
+
const dir = credentialDir(opts.stateDir);
|
|
649
|
+
const credPath = credentialFilePath(opts.stateDir);
|
|
650
|
+
const tokPath = tokenFilePath(opts.stateDir);
|
|
651
|
+
await fs4.mkdir(dir, { recursive: true, mode: 448 });
|
|
652
|
+
const credTmp = `${credPath}.tmp-${process.pid.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
653
|
+
const tokTmp = `${tokPath}.tmp-${process.pid.toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
654
|
+
await fs4.writeFile(credTmp, `${JSON.stringify(cred, null, 2)}
|
|
655
|
+
`, { mode: 384 });
|
|
656
|
+
await fs4.writeFile(tokTmp, `${cred.accessToken}
|
|
657
|
+
`, { mode: 384 });
|
|
658
|
+
try {
|
|
659
|
+
await fs4.chmod(credTmp, 384);
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
try {
|
|
663
|
+
await fs4.chmod(tokTmp, 384);
|
|
664
|
+
} catch {
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
await fs4.rename(credTmp, credPath);
|
|
668
|
+
await fs4.rename(tokTmp, tokPath);
|
|
669
|
+
} catch (e2) {
|
|
670
|
+
try {
|
|
671
|
+
await fs4.rm(credTmp, { force: true });
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
await fs4.rm(tokTmp, { force: true });
|
|
676
|
+
} catch {
|
|
677
|
+
}
|
|
678
|
+
throw e2;
|
|
679
|
+
}
|
|
680
|
+
try {
|
|
681
|
+
await fs4.chmod(dir, 448);
|
|
682
|
+
} catch {
|
|
683
|
+
}
|
|
684
|
+
}
|
|
639
685
|
function isMissing(err) {
|
|
640
686
|
return typeof err === "object" && err !== null && err.code === "ENOENT";
|
|
641
687
|
}
|
|
@@ -656,6 +702,9 @@ async function clearAuthToken(stateDir) {
|
|
|
656
702
|
} catch {
|
|
657
703
|
}
|
|
658
704
|
}
|
|
705
|
+
async function readCredentialFile(stateDir) {
|
|
706
|
+
return (await readCredentialFileRead(stateDir)).value;
|
|
707
|
+
}
|
|
659
708
|
async function readCredentialFileRead(stateDir) {
|
|
660
709
|
let raw;
|
|
661
710
|
try {
|
|
@@ -13820,7 +13869,7 @@ var require_compiler = __commonJS({
|
|
|
13820
13869
|
});
|
|
13821
13870
|
this._emitLine("root: root\n};");
|
|
13822
13871
|
};
|
|
13823
|
-
_proto.compile = function
|
|
13872
|
+
_proto.compile = function compile2(node, frame) {
|
|
13824
13873
|
var _compile = this["compile" + node.typename];
|
|
13825
13874
|
if (_compile) {
|
|
13826
13875
|
_compile.call(this, node, frame);
|
|
@@ -13834,7 +13883,7 @@ var require_compiler = __commonJS({
|
|
|
13834
13883
|
return Compiler2;
|
|
13835
13884
|
}(Obj);
|
|
13836
13885
|
module.exports = {
|
|
13837
|
-
compile: function
|
|
13886
|
+
compile: function compile2(src2, asyncFilters, extensions, name, opts) {
|
|
13838
13887
|
if (opts === void 0) {
|
|
13839
13888
|
opts = {};
|
|
13840
13889
|
}
|
|
@@ -15249,7 +15298,7 @@ var require_environment = __commonJS({
|
|
|
15249
15298
|
}
|
|
15250
15299
|
});
|
|
15251
15300
|
};
|
|
15252
|
-
_proto3.compile = function
|
|
15301
|
+
_proto3.compile = function compile2() {
|
|
15253
15302
|
if (!this.compiled) {
|
|
15254
15303
|
this._compile();
|
|
15255
15304
|
}
|
|
@@ -15746,7 +15795,7 @@ var require_nunjucks = __commonJS({
|
|
|
15746
15795
|
reset: function reset() {
|
|
15747
15796
|
e2 = void 0;
|
|
15748
15797
|
},
|
|
15749
|
-
compile: function
|
|
15798
|
+
compile: function compile2(src2, env, path, eagerCompile) {
|
|
15750
15799
|
if (!e2) {
|
|
15751
15800
|
configure();
|
|
15752
15801
|
}
|
|
@@ -15914,6 +15963,43 @@ function getLanguageAliasesForExtension(ext) {
|
|
|
15914
15963
|
function generateCompilationId() {
|
|
15915
15964
|
return `compile-${++compilationCounter}-${Date.now()}`;
|
|
15916
15965
|
}
|
|
15966
|
+
function normalizePosix2(p2) {
|
|
15967
|
+
const isAbs = p2.startsWith("/");
|
|
15968
|
+
const out = [];
|
|
15969
|
+
for (const seg of p2.split("/")) {
|
|
15970
|
+
if (seg === "" || seg === ".") continue;
|
|
15971
|
+
if (seg === "..") {
|
|
15972
|
+
if (out.length && out[out.length - 1] !== "..") out.pop();
|
|
15973
|
+
else if (!isAbs) out.push("..");
|
|
15974
|
+
} else {
|
|
15975
|
+
out.push(seg);
|
|
15976
|
+
}
|
|
15977
|
+
}
|
|
15978
|
+
const joined = out.join("/");
|
|
15979
|
+
return isAbs ? "/" + joined : joined || ".";
|
|
15980
|
+
}
|
|
15981
|
+
function joinPosix2(...segments2) {
|
|
15982
|
+
const filtered = segments2.filter((s2) => s2 && s2.length > 0);
|
|
15983
|
+
if (filtered.length === 0) return ".";
|
|
15984
|
+
return normalizePosix2(filtered.join("/"));
|
|
15985
|
+
}
|
|
15986
|
+
function dirnamePosix2(p2) {
|
|
15987
|
+
const norm2 = p2.replace(/\/+$/, "");
|
|
15988
|
+
const idx = norm2.lastIndexOf("/");
|
|
15989
|
+
if (idx === -1) return ".";
|
|
15990
|
+
if (idx === 0) return "/";
|
|
15991
|
+
return norm2.slice(0, idx);
|
|
15992
|
+
}
|
|
15993
|
+
function utf8ByteLength(s2) {
|
|
15994
|
+
return new TextEncoder().encode(s2).length;
|
|
15995
|
+
}
|
|
15996
|
+
function toRelKey(filePath) {
|
|
15997
|
+
let n2 = filePath.replace(/\\/g, "/");
|
|
15998
|
+
if (n2.startsWith("./")) n2 = n2.substring(2);
|
|
15999
|
+
if (n2.startsWith("/")) n2 = n2.substring(1);
|
|
16000
|
+
if (n2.endsWith("/") && n2.length > 1) n2 = n2.substring(0, n2.length - 1);
|
|
16001
|
+
return n2;
|
|
16002
|
+
}
|
|
15917
16003
|
function createCoreStages() {
|
|
15918
16004
|
return [
|
|
15919
16005
|
new LexicalAnalysisStage(),
|
|
@@ -15923,7 +16009,7 @@ function createCoreStages() {
|
|
|
15923
16009
|
new CodeGenerationStage()
|
|
15924
16010
|
];
|
|
15925
16011
|
}
|
|
15926
|
-
var yaml, import_semver, nunjucks, PrompdParser, PACKAGE_TYPE_DIRS, VALID_PACKAGE_TYPES, PrompdError, ParseError, ValidationError, CompilationError, SecurityError, CompilationContext, DEFAULT_SECURITY_CONFIG, PROMPD_EXTENSIONS, CompilerPipeline, LexicalAnalysisStage, DependencyResolutionStage, SemanticAnalysisStage, _SectionOverrideProcessor, SectionOverrideProcessor, EXTENSION_TO_LANGUAGE_ALIASES, globalIncludeStacks, compilationCounter, PrompdLoader, TemplateProcessingStage, MarkdownFormatter, OpenAIFormatter, AnthropicFormatter, CodeGenerationStage, PrompdCompiler;
|
|
16012
|
+
var yaml, import_semver, nunjucks, PrompdParser, PACKAGE_TYPE_DIRS, VALID_PACKAGE_TYPES, PrompdError, ParseError, ValidationError, CompilationError, SecurityError, CompilationContext, DEFAULT_SECURITY_CONFIG, PROMPD_EXTENSIONS, CompilerPipeline, LexicalAnalysisStage, DependencyResolutionStage, SemanticAnalysisStage, _SectionOverrideProcessor, SectionOverrideProcessor, EXTENSION_TO_LANGUAGE_ALIASES, globalIncludeStacks, compilationCounter, PrompdLoader, TemplateProcessingStage, MarkdownFormatter, OpenAIFormatter, AnthropicFormatter, CodeGenerationStage, MemoryFileSystem, HybridFileSystem, PrompdCompiler;
|
|
15927
16013
|
var init_dist = __esm({
|
|
15928
16014
|
"../../node_modules/.pnpm/@prompd+core@0.5.0-beta.10/node_modules/@prompd/core/dist/index.js"() {
|
|
15929
16015
|
yaml = __toESM(require_dist(), 1);
|
|
@@ -17205,8 +17291,8 @@ ${yamlContent}` : yamlContent;
|
|
|
17205
17291
|
processedContent = content;
|
|
17206
17292
|
}
|
|
17207
17293
|
if (this.verbose) {
|
|
17208
|
-
const
|
|
17209
|
-
console.log(`[include] Loaded ${ext} file: ${filePath} (${
|
|
17294
|
+
const preview2 = processedContent.substring(0, 50).replace(/\n/g, "\\n");
|
|
17295
|
+
console.log(`[include] Loaded ${ext} file: ${filePath} (${preview2}...)`);
|
|
17210
17296
|
}
|
|
17211
17297
|
return {
|
|
17212
17298
|
src: processedContent,
|
|
@@ -17828,8 +17914,8 @@ ${content}` : parentData.content;
|
|
|
17828
17914
|
}
|
|
17829
17915
|
removedCount++;
|
|
17830
17916
|
if (verbose) {
|
|
17831
|
-
const
|
|
17832
|
-
console.log(` - Removing \`\`\`${lang} block: "${
|
|
17917
|
+
const preview2 = blockContent.substring(0, 50).replace(/\n/g, "\\n");
|
|
17918
|
+
console.log(` - Removing \`\`\`${lang} block: "${preview2}..."`);
|
|
17833
17919
|
}
|
|
17834
17920
|
return "";
|
|
17835
17921
|
});
|
|
@@ -18503,6 +18589,150 @@ ${firstUserMsg.content}`;
|
|
|
18503
18589
|
return "Code Generation";
|
|
18504
18590
|
}
|
|
18505
18591
|
};
|
|
18592
|
+
MemoryFileSystem = class {
|
|
18593
|
+
constructor(files = {}) {
|
|
18594
|
+
this.files = /* @__PURE__ */ new Map();
|
|
18595
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
18596
|
+
this.addFile(filePath, content);
|
|
18597
|
+
}
|
|
18598
|
+
}
|
|
18599
|
+
/** Add or update a file in the in-memory file system. */
|
|
18600
|
+
addFile(filePath, content) {
|
|
18601
|
+
this.files.set(this.normalizePath(filePath), content);
|
|
18602
|
+
}
|
|
18603
|
+
/** Add multiple files at once. */
|
|
18604
|
+
addFiles(files) {
|
|
18605
|
+
for (const [filePath, content] of Object.entries(files)) {
|
|
18606
|
+
this.addFile(filePath, content);
|
|
18607
|
+
}
|
|
18608
|
+
}
|
|
18609
|
+
exists(filePath) {
|
|
18610
|
+
return this.files.has(this.normalizePath(filePath));
|
|
18611
|
+
}
|
|
18612
|
+
readFile(filePath) {
|
|
18613
|
+
const normalizedPath = this.normalizePath(filePath);
|
|
18614
|
+
const content = this.files.get(normalizedPath);
|
|
18615
|
+
if (content === void 0) {
|
|
18616
|
+
throw new Error(`File not found: ${filePath}`);
|
|
18617
|
+
}
|
|
18618
|
+
return content;
|
|
18619
|
+
}
|
|
18620
|
+
isDirectory(filePath) {
|
|
18621
|
+
const normalizedPath = this.normalizePath(filePath);
|
|
18622
|
+
const dirPrefix = normalizedPath.endsWith("/") ? normalizedPath : normalizedPath + "/";
|
|
18623
|
+
for (const file of this.files.keys()) {
|
|
18624
|
+
if (file.startsWith(dirPrefix)) {
|
|
18625
|
+
return true;
|
|
18626
|
+
}
|
|
18627
|
+
}
|
|
18628
|
+
return false;
|
|
18629
|
+
}
|
|
18630
|
+
readdir(dirPath) {
|
|
18631
|
+
const normalizedDir = this.normalizePath(dirPath);
|
|
18632
|
+
const dirPrefix = normalizedDir.endsWith("/") ? normalizedDir : normalizedDir + "/";
|
|
18633
|
+
const files = /* @__PURE__ */ new Set();
|
|
18634
|
+
for (const file of this.files.keys()) {
|
|
18635
|
+
if (file.startsWith(dirPrefix)) {
|
|
18636
|
+
const relativePath = file.substring(dirPrefix.length);
|
|
18637
|
+
const slashIndex = relativePath.indexOf("/");
|
|
18638
|
+
const fileName = slashIndex === -1 ? relativePath : relativePath.substring(0, slashIndex);
|
|
18639
|
+
if (fileName) {
|
|
18640
|
+
files.add(fileName);
|
|
18641
|
+
}
|
|
18642
|
+
}
|
|
18643
|
+
}
|
|
18644
|
+
return Array.from(files);
|
|
18645
|
+
}
|
|
18646
|
+
resolve(...pathSegments) {
|
|
18647
|
+
return joinPosix2("/", ...pathSegments);
|
|
18648
|
+
}
|
|
18649
|
+
dirname(filePath) {
|
|
18650
|
+
return dirnamePosix2(filePath);
|
|
18651
|
+
}
|
|
18652
|
+
join(...pathSegments) {
|
|
18653
|
+
return joinPosix2(...pathSegments);
|
|
18654
|
+
}
|
|
18655
|
+
/** Get the virtual file system path for a package. */
|
|
18656
|
+
getPackagePath(packageName, version) {
|
|
18657
|
+
return `/packages/${packageName}@${version}`;
|
|
18658
|
+
}
|
|
18659
|
+
/** Get all files under an optional base path. */
|
|
18660
|
+
getAllFiles(basePath) {
|
|
18661
|
+
if (!basePath) {
|
|
18662
|
+
return new Map(this.files);
|
|
18663
|
+
}
|
|
18664
|
+
const normalizedBase = this.normalizePath(basePath);
|
|
18665
|
+
const prefix = normalizedBase.endsWith("/") ? normalizedBase : normalizedBase + "/";
|
|
18666
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
18667
|
+
for (const [filePath, content] of this.files.entries()) {
|
|
18668
|
+
if (filePath.startsWith(prefix) || filePath === normalizedBase) {
|
|
18669
|
+
filtered.set(filePath, content);
|
|
18670
|
+
}
|
|
18671
|
+
}
|
|
18672
|
+
return filtered;
|
|
18673
|
+
}
|
|
18674
|
+
/** Calculate total size (bytes) and file count under a base path. */
|
|
18675
|
+
getTotalSize(basePath) {
|
|
18676
|
+
const files = this.getAllFiles(basePath);
|
|
18677
|
+
let totalSize = 0;
|
|
18678
|
+
for (const content of files.values()) {
|
|
18679
|
+
totalSize += utf8ByteLength(content);
|
|
18680
|
+
}
|
|
18681
|
+
return { size: totalSize, files: files.size };
|
|
18682
|
+
}
|
|
18683
|
+
/** Normalize path to forward slashes, drop leading ./ and /, no trailing slash. */
|
|
18684
|
+
normalizePath(filePath) {
|
|
18685
|
+
return toRelKey(filePath);
|
|
18686
|
+
}
|
|
18687
|
+
};
|
|
18688
|
+
HybridFileSystem = class {
|
|
18689
|
+
constructor(files = {}, backend2) {
|
|
18690
|
+
this.mem = new MemoryFileSystem(files);
|
|
18691
|
+
this.backend = backend2;
|
|
18692
|
+
}
|
|
18693
|
+
/** Add or update an in-memory (synchronously-served) file. */
|
|
18694
|
+
addFile(filePath, content) {
|
|
18695
|
+
this.mem.addFile(filePath, content);
|
|
18696
|
+
}
|
|
18697
|
+
/** Add multiple in-memory files at once. */
|
|
18698
|
+
addFiles(files) {
|
|
18699
|
+
this.mem.addFiles(files);
|
|
18700
|
+
}
|
|
18701
|
+
exists(filePath) {
|
|
18702
|
+
if (this.mem.exists(filePath)) return true;
|
|
18703
|
+
return this.backend.readFile(this.normalize(filePath)).then((content) => content !== null).catch(() => false);
|
|
18704
|
+
}
|
|
18705
|
+
readFile(filePath) {
|
|
18706
|
+
if (this.mem.exists(filePath)) return this.mem.readFile(filePath);
|
|
18707
|
+
return this.backend.readFile(this.normalize(filePath)).then((content) => {
|
|
18708
|
+
if (content === null) throw new Error(`File not found: ${filePath}`);
|
|
18709
|
+
return content;
|
|
18710
|
+
});
|
|
18711
|
+
}
|
|
18712
|
+
isDirectory(filePath) {
|
|
18713
|
+
if (this.mem.isDirectory(filePath)) return true;
|
|
18714
|
+
if (!this.backend.isDirectory) return false;
|
|
18715
|
+
return this.backend.isDirectory(this.normalize(filePath)).catch(() => false);
|
|
18716
|
+
}
|
|
18717
|
+
readdir(dirPath) {
|
|
18718
|
+
const local = this.mem.readdir(dirPath);
|
|
18719
|
+
if (!this.backend.readdir) return local;
|
|
18720
|
+
return this.backend.readdir(this.normalize(dirPath)).then((remote) => Array.from(/* @__PURE__ */ new Set([...local, ...remote]))).catch(() => local);
|
|
18721
|
+
}
|
|
18722
|
+
resolve(...pathSegments) {
|
|
18723
|
+
return this.mem.resolve(...pathSegments);
|
|
18724
|
+
}
|
|
18725
|
+
dirname(filePath) {
|
|
18726
|
+
return this.mem.dirname(filePath);
|
|
18727
|
+
}
|
|
18728
|
+
join(...pathSegments) {
|
|
18729
|
+
return this.mem.join(...pathSegments);
|
|
18730
|
+
}
|
|
18731
|
+
/** Normalize to the same relative keys MemoryFileSystem uses, for backend lookups. */
|
|
18732
|
+
normalize(filePath) {
|
|
18733
|
+
return toRelKey(filePath);
|
|
18734
|
+
}
|
|
18735
|
+
};
|
|
18506
18736
|
PrompdCompiler = class {
|
|
18507
18737
|
constructor(options = {}) {
|
|
18508
18738
|
this.securityConfig = options.securityConfig || DEFAULT_SECURITY_CONFIG;
|
|
@@ -18535,13 +18765,107 @@ ${firstUserMsg.content}`;
|
|
|
18535
18765
|
}
|
|
18536
18766
|
});
|
|
18537
18767
|
|
|
18768
|
+
// ../tools-node/toolGuidance.ts
|
|
18769
|
+
function nodeToolGuidance(toolNames) {
|
|
18770
|
+
const has3 = new Set(toolNames);
|
|
18771
|
+
const parts = [];
|
|
18772
|
+
if (has3.has(SHELL_TOOL)) parts.push(SHELL);
|
|
18773
|
+
if (PROC_TOOLS.some((t2) => has3.has(t2))) parts.push(PROCS);
|
|
18774
|
+
return parts.join("\n\n");
|
|
18775
|
+
}
|
|
18776
|
+
var SHELL_TOOL, PROC_TOOLS, SHELL, PROCS;
|
|
18777
|
+
var init_toolGuidance = __esm({
|
|
18778
|
+
"../tools-node/toolGuidance.ts"() {
|
|
18779
|
+
"use strict";
|
|
18780
|
+
SHELL_TOOL = "shell";
|
|
18781
|
+
PROC_TOOLS = ["proc_start", "proc_stop", "proc_list"];
|
|
18782
|
+
SHELL = `Running a command is often the shortest correct answer: use \`${SHELL_TOOL}\` for anything a command already does well -- listing, searching, git, package managers, build and test runners -- instead of reconstructing it from file reads. It waits for the command to finish and returns its output, so it is the wrong tool for anything that does not exit on its own.`;
|
|
18783
|
+
PROCS = `A command that keeps running (a dev server, a watcher, a queue worker) belongs to \`${PROC_TOOLS[0]}\`, not \`${SHELL_TOOL}\` -- starting one with \`${SHELL_TOOL}\` hangs the turn until it is killed. Use \`${PROC_TOOLS[2]}\` to see what is already running before starting anything, and \`${PROC_TOOLS[1]}\` to stop what you started. Do not stop a process you did not start.`;
|
|
18784
|
+
}
|
|
18785
|
+
});
|
|
18786
|
+
|
|
18538
18787
|
// ../tools-node/promptHost.ts
|
|
18788
|
+
function backend(svc) {
|
|
18789
|
+
return {
|
|
18790
|
+
async readFile(p2) {
|
|
18791
|
+
try {
|
|
18792
|
+
return await svc.read(normalizeFilePath(p2));
|
|
18793
|
+
} catch {
|
|
18794
|
+
return null;
|
|
18795
|
+
}
|
|
18796
|
+
},
|
|
18797
|
+
async isDirectory(p2) {
|
|
18798
|
+
return (await svc.stat(normalizeFilePath(p2)))?.kind === "directory";
|
|
18799
|
+
},
|
|
18800
|
+
async readdir(p2) {
|
|
18801
|
+
return (await svc.list(normalizeFilePath(p2))).map((n2) => n2.name);
|
|
18802
|
+
}
|
|
18803
|
+
};
|
|
18804
|
+
}
|
|
18805
|
+
async function preload(svc) {
|
|
18806
|
+
const out = {};
|
|
18807
|
+
for (const f2 of flattenFiles(await svc.list())) {
|
|
18808
|
+
try {
|
|
18809
|
+
out[f2.path] = await svc.read(f2.path);
|
|
18810
|
+
} catch {
|
|
18811
|
+
}
|
|
18812
|
+
}
|
|
18813
|
+
return out;
|
|
18814
|
+
}
|
|
18815
|
+
async function compile(src2, values, fileSystem, mainPath) {
|
|
18816
|
+
const mp = mainPath || "/main.prmd";
|
|
18817
|
+
const fs17 = fileSystem || new MemoryFileSystem({ [mp]: src2 });
|
|
18818
|
+
const ctx = await compiler.compileWithContext(mp, { parameters: values || {}, fileSystem: fs17, outputFormat: "markdown" });
|
|
18819
|
+
const compiled = ctx.compiledResult;
|
|
18820
|
+
const output = typeof compiled === "string" ? compiled : compiled ? new TextDecoder().decode(compiled) : "";
|
|
18821
|
+
return { output };
|
|
18822
|
+
}
|
|
18823
|
+
function nodePromptHost(workspace, project) {
|
|
18824
|
+
return {
|
|
18825
|
+
workspace,
|
|
18826
|
+
/* The user's PROJECT files, when the caller has them, so skills installed
|
|
18827
|
+
* into `.prompd/skills/` are visible. Omitted rather than faked when there
|
|
18828
|
+
* is none: a CLI invoked outside a project genuinely has no installed
|
|
18829
|
+
* skills, and saying so beats inventing an empty directory. */
|
|
18830
|
+
...project ? { projectWorkspace: () => project } : {},
|
|
18831
|
+
seed: async (defaults2) => {
|
|
18832
|
+
for (const [p2, content] of Object.entries(defaults2)) {
|
|
18833
|
+
if (!await workspace.stat(p2)) await workspace.write(p2, content);
|
|
18834
|
+
}
|
|
18835
|
+
},
|
|
18836
|
+
parse: (source) => ({ meta: parser.parseContent(source).metadata ?? {} }),
|
|
18837
|
+
compileBody: (source, values) => compile(source, values),
|
|
18838
|
+
compileTemplate: async (path, liveSrc, values) => {
|
|
18839
|
+
const sources = await preload(workspace);
|
|
18840
|
+
const p2 = normalizeFilePath(path);
|
|
18841
|
+
sources[p2] = liveSrc;
|
|
18842
|
+
return compile("", values, new HybridFileSystem(sources, backend(workspace)), "/" + p2);
|
|
18843
|
+
},
|
|
18844
|
+
siteIndex: () => "",
|
|
18845
|
+
// no site index in node (the help chat is browser-only)
|
|
18846
|
+
/* Who the agent says it IS. Without this, `composeAgentSystem` falls back
|
|
18847
|
+
* to its `'Prompd'` default and the `corenel` binary's agent introduced
|
|
18848
|
+
* itself as "the Prompd assistant" -- the same leak already fixed for the
|
|
18849
|
+
* browser help chat (composeHelpSystem) and missed here, because the node
|
|
18850
|
+
* host is the one PromptHost that never implemented the hook.
|
|
18851
|
+
*
|
|
18852
|
+
* Constant, not a parameter: every node consumer of this host (the CLI and
|
|
18853
|
+
* the sidecar) IS Corenel. A host that is not should implement its own. */
|
|
18854
|
+
appName: () => "Corenel",
|
|
18855
|
+
/* The product's guidance for the tools this run actually has -- the shell,
|
|
18856
|
+
* long-running processes, and deferred MCP tools. Gated per tool inside
|
|
18857
|
+
* nodeToolGuidance, because this same host serves the sidecar, whose shell
|
|
18858
|
+
* is behind --allow-shell. */
|
|
18859
|
+
toolGuidance: (names) => nodeToolGuidance(names)
|
|
18860
|
+
};
|
|
18861
|
+
}
|
|
18539
18862
|
var parser, compiler;
|
|
18540
18863
|
var init_promptHost = __esm({
|
|
18541
18864
|
"../tools-node/promptHost.ts"() {
|
|
18542
18865
|
"use strict";
|
|
18543
18866
|
init_dist();
|
|
18544
18867
|
init_protocol();
|
|
18868
|
+
init_toolGuidance();
|
|
18545
18869
|
parser = new PrompdParser();
|
|
18546
18870
|
compiler = new PrompdCompiler();
|
|
18547
18871
|
}
|
|
@@ -18550,7 +18874,7 @@ var init_promptHost = __esm({
|
|
|
18550
18874
|
// ../tools-node/migrateHome.ts
|
|
18551
18875
|
import { promises as fs5 } from "node:fs";
|
|
18552
18876
|
import { homedir as homedir3 } from "node:os";
|
|
18553
|
-
import { join as
|
|
18877
|
+
import { join as join6, dirname as dirname3 } from "node:path";
|
|
18554
18878
|
async function isDir(p2) {
|
|
18555
18879
|
return fs5.stat(p2).then((s2) => s2.isDirectory()).catch(() => false);
|
|
18556
18880
|
}
|
|
@@ -18566,7 +18890,7 @@ async function moveEntry(from, to, report) {
|
|
|
18566
18890
|
report.skipped.push({ path: to, reason: "destination already exists" });
|
|
18567
18891
|
return;
|
|
18568
18892
|
}
|
|
18569
|
-
await fs5.mkdir(
|
|
18893
|
+
await fs5.mkdir(dirname3(to), { recursive: true });
|
|
18570
18894
|
try {
|
|
18571
18895
|
await fs5.rename(from, to);
|
|
18572
18896
|
report.moved.push(to);
|
|
@@ -18592,8 +18916,8 @@ async function moveEntry(from, to, report) {
|
|
|
18592
18916
|
async function mergeDir(from, to, report) {
|
|
18593
18917
|
const entries = await fs5.readdir(from, { withFileTypes: true }).catch(() => []);
|
|
18594
18918
|
for (const e2 of entries) {
|
|
18595
|
-
const src2 =
|
|
18596
|
-
const dst =
|
|
18919
|
+
const src2 = join6(from, e2.name);
|
|
18920
|
+
const dst = join6(to, e2.name);
|
|
18597
18921
|
if (e2.isDirectory() && await isDir(dst)) await mergeDir(src2, dst, report);
|
|
18598
18922
|
else await moveEntry(src2, dst, report);
|
|
18599
18923
|
}
|
|
@@ -18604,16 +18928,16 @@ async function migrateHomeLayout(base = homedir3()) {
|
|
|
18604
18928
|
const report = { moved: [], skipped: [] };
|
|
18605
18929
|
const home = corenelDir2(base);
|
|
18606
18930
|
if (!await present(home)) return report;
|
|
18607
|
-
const config =
|
|
18931
|
+
const config = join6(home, "config");
|
|
18608
18932
|
if (await isDir(config)) await mergeDir(config, home, report);
|
|
18609
18933
|
const side = sidecarDir(base);
|
|
18610
18934
|
for (const name of SIDECAR_FILES) {
|
|
18611
|
-
await moveEntry(
|
|
18935
|
+
await moveEntry(join6(home, name), join6(side, name), report);
|
|
18612
18936
|
}
|
|
18613
18937
|
for (const name of SIDECAR_DIRS) {
|
|
18614
|
-
const src2 =
|
|
18938
|
+
const src2 = join6(home, name);
|
|
18615
18939
|
if (!await isDir(src2)) continue;
|
|
18616
|
-
const dst =
|
|
18940
|
+
const dst = join6(side, name);
|
|
18617
18941
|
if (await isDir(dst)) await mergeDir(src2, dst, report);
|
|
18618
18942
|
else await moveEntry(src2, dst, report);
|
|
18619
18943
|
}
|
|
@@ -18638,7 +18962,50 @@ var init_migrateHome = __esm({
|
|
|
18638
18962
|
}
|
|
18639
18963
|
});
|
|
18640
18964
|
|
|
18965
|
+
// ../tools-node/openBrowser.ts
|
|
18966
|
+
import { spawn } from "node:child_process";
|
|
18967
|
+
function openBrowser(url) {
|
|
18968
|
+
const [cmd, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
18969
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
18970
|
+
child.on("error", () => {
|
|
18971
|
+
});
|
|
18972
|
+
child.unref();
|
|
18973
|
+
}
|
|
18974
|
+
var init_openBrowser = __esm({
|
|
18975
|
+
"../tools-node/openBrowser.ts"() {
|
|
18976
|
+
"use strict";
|
|
18977
|
+
}
|
|
18978
|
+
});
|
|
18979
|
+
|
|
18641
18980
|
// ../tools-node/index.ts
|
|
18981
|
+
var tools_node_exports = {};
|
|
18982
|
+
__export(tools_node_exports, {
|
|
18983
|
+
API_BASE_ENV: () => API_BASE_ENV,
|
|
18984
|
+
NodeFileService: () => NodeFileService,
|
|
18985
|
+
TOKEN_ENV: () => TOKEN_ENV,
|
|
18986
|
+
authTokenPath: () => authTokenPath,
|
|
18987
|
+
clearAllCredentials: () => clearAllCredentials,
|
|
18988
|
+
clearAuthToken: () => clearAuthToken,
|
|
18989
|
+
corenelDir: () => corenelDir2,
|
|
18990
|
+
credentialDir: () => credentialDir,
|
|
18991
|
+
credentialFilePath: () => credentialFilePath,
|
|
18992
|
+
maskAccount: () => maskAccount,
|
|
18993
|
+
maskToken: () => maskToken,
|
|
18994
|
+
migrateHomeLayout: () => migrateHomeLayout,
|
|
18995
|
+
nodeAuthToken: () => nodeAuthToken,
|
|
18996
|
+
nodePromptHost: () => nodePromptHost,
|
|
18997
|
+
openBrowser: () => openBrowser,
|
|
18998
|
+
readAuthToken: () => readAuthToken,
|
|
18999
|
+
readAuthTokenRead: () => readAuthTokenRead,
|
|
19000
|
+
readCredentialFile: () => readCredentialFile,
|
|
19001
|
+
readCredentialFileRead: () => readCredentialFileRead,
|
|
19002
|
+
resolveApiBase: () => resolveApiBase,
|
|
19003
|
+
resolveNodeCredential: () => resolveNodeCredential,
|
|
19004
|
+
sidecarDir: () => sidecarDir,
|
|
19005
|
+
tokenFilePath: () => tokenFilePath,
|
|
19006
|
+
writeAuthToken: () => writeAuthToken,
|
|
19007
|
+
writeLoginCredential: () => writeLoginCredential
|
|
19008
|
+
});
|
|
18642
19009
|
var init_tools_node = __esm({
|
|
18643
19010
|
"../tools-node/index.ts"() {
|
|
18644
19011
|
"use strict";
|
|
@@ -18646,59 +19013,7 @@ var init_tools_node = __esm({
|
|
|
18646
19013
|
init_auth();
|
|
18647
19014
|
init_promptHost();
|
|
18648
19015
|
init_migrateHome();
|
|
18649
|
-
|
|
18650
|
-
});
|
|
18651
|
-
|
|
18652
|
-
// src/auth/store.ts
|
|
18653
|
-
import { promises as fs6 } from "node:fs";
|
|
18654
|
-
function authDir(stateDir) {
|
|
18655
|
-
return credentialDir(stateDir);
|
|
18656
|
-
}
|
|
18657
|
-
function credentialPath(stateDir) {
|
|
18658
|
-
return credentialFilePath(stateDir);
|
|
18659
|
-
}
|
|
18660
|
-
async function readForeignAuthJson(path) {
|
|
18661
|
-
let raw;
|
|
18662
|
-
try {
|
|
18663
|
-
raw = await fs6.readFile(path, "utf8");
|
|
18664
|
-
} catch {
|
|
18665
|
-
return {};
|
|
18666
|
-
}
|
|
18667
|
-
try {
|
|
18668
|
-
const parsed = JSON.parse(raw);
|
|
18669
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
18670
|
-
const obj = { ...parsed };
|
|
18671
|
-
for (const k2 of STORED_CREDENTIAL_KEYS) delete obj[k2];
|
|
18672
|
-
return obj;
|
|
18673
|
-
} catch {
|
|
18674
|
-
return {};
|
|
18675
|
-
}
|
|
18676
|
-
}
|
|
18677
|
-
async function saveCredential(cred, stateDir) {
|
|
18678
|
-
const path = credentialPath(stateDir);
|
|
18679
|
-
await fs6.mkdir(authDir(stateDir), { recursive: true });
|
|
18680
|
-
const foreign = await readForeignAuthJson(path);
|
|
18681
|
-
const next = { ...foreign, ...cred };
|
|
18682
|
-
await fs6.writeFile(path, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
18683
|
-
try {
|
|
18684
|
-
await fs6.chmod(path, 384);
|
|
18685
|
-
} catch {
|
|
18686
|
-
}
|
|
18687
|
-
await writeAuthToken(cred.accessToken, stateDir);
|
|
18688
|
-
}
|
|
18689
|
-
async function clearCredential(stateDir) {
|
|
18690
|
-
await clearAllCredentials(stateDir);
|
|
18691
|
-
}
|
|
18692
|
-
async function resolveCredential(env = process.env, stateDir, now2 = Date.now) {
|
|
18693
|
-
return await resolveNodeCredential({ env, now: now2, ...stateDir ? { stateDir } : {} });
|
|
18694
|
-
}
|
|
18695
|
-
var TOKEN_ENV2, STORED_CREDENTIAL_KEYS;
|
|
18696
|
-
var init_store = __esm({
|
|
18697
|
-
"src/auth/store.ts"() {
|
|
18698
|
-
"use strict";
|
|
18699
|
-
init_tools_node();
|
|
18700
|
-
TOKEN_ENV2 = TOKEN_ENV;
|
|
18701
|
-
STORED_CREDENTIAL_KEYS = ["accessToken", "refreshToken", "expiresAt", "account", "apiBase"];
|
|
19016
|
+
init_openBrowser();
|
|
18702
19017
|
}
|
|
18703
19018
|
});
|
|
18704
19019
|
|
|
@@ -25102,270 +25417,6 @@ var init_fileFromPath = __esm({
|
|
|
25102
25417
|
}
|
|
25103
25418
|
});
|
|
25104
25419
|
|
|
25105
|
-
// src/auth/pkce.ts
|
|
25106
|
-
import { createHash as createHash4, randomBytes as randomBytes6 } from "node:crypto";
|
|
25107
|
-
function base64url2(buf) {
|
|
25108
|
-
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
25109
|
-
}
|
|
25110
|
-
function createVerifier(bytes = 32) {
|
|
25111
|
-
return base64url2(randomBytes6(bytes));
|
|
25112
|
-
}
|
|
25113
|
-
function challengeFor(verifier) {
|
|
25114
|
-
return base64url2(createHash4("sha256").update(verifier).digest());
|
|
25115
|
-
}
|
|
25116
|
-
function createState() {
|
|
25117
|
-
return base64url2(randomBytes6(16));
|
|
25118
|
-
}
|
|
25119
|
-
function createPkce() {
|
|
25120
|
-
const verifier = createVerifier();
|
|
25121
|
-
return { verifier, challenge: challengeFor(verifier), method: "S256", state: createState() };
|
|
25122
|
-
}
|
|
25123
|
-
function authorizeUrl(p2) {
|
|
25124
|
-
const u2 = new URL("/oauth/authorize", p2.issuer);
|
|
25125
|
-
u2.searchParams.set("response_type", "code");
|
|
25126
|
-
u2.searchParams.set("client_id", p2.clientId);
|
|
25127
|
-
u2.searchParams.set("redirect_uri", p2.redirectUri);
|
|
25128
|
-
u2.searchParams.set("code_challenge", p2.pkce.challenge);
|
|
25129
|
-
u2.searchParams.set("code_challenge_method", p2.pkce.method);
|
|
25130
|
-
u2.searchParams.set("state", p2.pkce.state);
|
|
25131
|
-
u2.searchParams.set("scope", p2.scope ?? "profile email");
|
|
25132
|
-
return u2.toString();
|
|
25133
|
-
}
|
|
25134
|
-
function stateMatches(expected, got) {
|
|
25135
|
-
if (!got || got.length !== expected.length) return false;
|
|
25136
|
-
let diff = 0;
|
|
25137
|
-
for (let i2 = 0; i2 < expected.length; i2++) diff |= expected.charCodeAt(i2) ^ got.charCodeAt(i2);
|
|
25138
|
-
return diff === 0;
|
|
25139
|
-
}
|
|
25140
|
-
var init_pkce = __esm({
|
|
25141
|
-
"src/auth/pkce.ts"() {
|
|
25142
|
-
"use strict";
|
|
25143
|
-
}
|
|
25144
|
-
});
|
|
25145
|
-
|
|
25146
|
-
// src/auth/login.ts
|
|
25147
|
-
import { createServer as createServer2 } from "node:http";
|
|
25148
|
-
function awaitCallback(pkce, port, timeoutMs) {
|
|
25149
|
-
let resolveUri;
|
|
25150
|
-
let resolveResult;
|
|
25151
|
-
let rejectResult;
|
|
25152
|
-
const redirectUri = new Promise((res) => {
|
|
25153
|
-
resolveUri = res;
|
|
25154
|
-
});
|
|
25155
|
-
const result = new Promise((res, rej) => {
|
|
25156
|
-
resolveResult = res;
|
|
25157
|
-
rejectResult = rej;
|
|
25158
|
-
});
|
|
25159
|
-
const server = createServer2((req, res) => {
|
|
25160
|
-
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
25161
|
-
if (url.pathname !== "/callback") {
|
|
25162
|
-
res.writeHead(404).end();
|
|
25163
|
-
return;
|
|
25164
|
-
}
|
|
25165
|
-
const err = url.searchParams.get("error");
|
|
25166
|
-
const code2 = url.searchParams.get("code");
|
|
25167
|
-
const state = url.searchParams.get("state");
|
|
25168
|
-
if (!stateMatches(pkce.state, state)) {
|
|
25169
|
-
res.writeHead(400, { "content-type": "text/html" }).end(PAGE_FAIL2);
|
|
25170
|
-
rejectResult(new Error("state mismatch \u2014 the callback did not match this sign-in request"));
|
|
25171
|
-
return;
|
|
25172
|
-
}
|
|
25173
|
-
if (err) {
|
|
25174
|
-
res.writeHead(400, { "content-type": "text/html" }).end(PAGE_FAIL2);
|
|
25175
|
-
rejectResult(new Error(`authorization failed: ${err}`));
|
|
25176
|
-
return;
|
|
25177
|
-
}
|
|
25178
|
-
if (!code2) {
|
|
25179
|
-
res.writeHead(400, { "content-type": "text/html" }).end(PAGE_FAIL2);
|
|
25180
|
-
rejectResult(new Error("callback carried no authorization code"));
|
|
25181
|
-
return;
|
|
25182
|
-
}
|
|
25183
|
-
res.writeHead(200, { "content-type": "text/html" }).end(PAGE_OK2);
|
|
25184
|
-
resolveResult({ code: code2 });
|
|
25185
|
-
});
|
|
25186
|
-
const timer = setTimeout(() => {
|
|
25187
|
-
rejectResult(new Error("timed out waiting for the browser callback"));
|
|
25188
|
-
}, timeoutMs);
|
|
25189
|
-
timer.unref?.();
|
|
25190
|
-
const close2 = () => {
|
|
25191
|
-
clearTimeout(timer);
|
|
25192
|
-
server.close();
|
|
25193
|
-
};
|
|
25194
|
-
void result.finally(close2).catch(() => {
|
|
25195
|
-
});
|
|
25196
|
-
server.listen(port, "127.0.0.1", () => {
|
|
25197
|
-
const addr = server.address();
|
|
25198
|
-
const bound = typeof addr === "object" && addr ? addr.port : port;
|
|
25199
|
-
resolveUri(`http://127.0.0.1:${bound}/callback`);
|
|
25200
|
-
});
|
|
25201
|
-
return { redirectUri, result, close: close2 };
|
|
25202
|
-
}
|
|
25203
|
-
async function login(deps) {
|
|
25204
|
-
const log = deps.log ?? (() => void 0);
|
|
25205
|
-
const now2 = deps.now ?? Date.now;
|
|
25206
|
-
const pkce = createPkce();
|
|
25207
|
-
const { redirectUri: uriP, result, close: close2 } = awaitCallback(
|
|
25208
|
-
pkce,
|
|
25209
|
-
deps.config.port ?? 0,
|
|
25210
|
-
deps.timeoutMs ?? DEFAULT_TIMEOUT_MS4
|
|
25211
|
-
);
|
|
25212
|
-
try {
|
|
25213
|
-
const redirectUri = await uriP;
|
|
25214
|
-
const url = authorizeUrl({
|
|
25215
|
-
issuer: deps.config.issuer,
|
|
25216
|
-
clientId: deps.config.clientId,
|
|
25217
|
-
redirectUri,
|
|
25218
|
-
pkce,
|
|
25219
|
-
...deps.config.scope ? { scope: deps.config.scope } : {}
|
|
25220
|
-
});
|
|
25221
|
-
log("Opening your browser to sign in...");
|
|
25222
|
-
log(url);
|
|
25223
|
-
await deps.openBrowser(url);
|
|
25224
|
-
const { code: code2 } = await result;
|
|
25225
|
-
const tokens = await deps.exchange({
|
|
25226
|
-
issuer: deps.config.issuer,
|
|
25227
|
-
clientId: deps.config.clientId,
|
|
25228
|
-
code: code2,
|
|
25229
|
-
verifier: pkce.verifier,
|
|
25230
|
-
redirectUri
|
|
25231
|
-
});
|
|
25232
|
-
if (!tokens.access_token) throw new Error("the token exchange returned no access token");
|
|
25233
|
-
const cred = {
|
|
25234
|
-
accessToken: tokens.access_token,
|
|
25235
|
-
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {},
|
|
25236
|
-
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
25237
|
-
...tokens.account ? { account: tokens.account } : {},
|
|
25238
|
-
...deps.apiBase ? { apiBase: deps.apiBase } : {}
|
|
25239
|
-
};
|
|
25240
|
-
await saveCredential(cred, deps.stateDir);
|
|
25241
|
-
return {
|
|
25242
|
-
...cred.account ? { account: cred.account } : {},
|
|
25243
|
-
...cred.expiresAt ? { expiresAt: cred.expiresAt } : {}
|
|
25244
|
-
};
|
|
25245
|
-
} finally {
|
|
25246
|
-
close2();
|
|
25247
|
-
}
|
|
25248
|
-
}
|
|
25249
|
-
var DEFAULT_TIMEOUT_MS4, PAGE_OK2, PAGE_FAIL2;
|
|
25250
|
-
var init_login = __esm({
|
|
25251
|
-
"src/auth/login.ts"() {
|
|
25252
|
-
"use strict";
|
|
25253
|
-
init_pkce();
|
|
25254
|
-
init_store();
|
|
25255
|
-
DEFAULT_TIMEOUT_MS4 = 5 * 60 * 1e3;
|
|
25256
|
-
PAGE_OK2 = `<!doctype html><meta charset="utf-8"><title>Signed in</title>
|
|
25257
|
-
<body style="font:16px system-ui;padding:3rem;text-align:center">
|
|
25258
|
-
<h1>Signed in to Corenel</h1><p>You can close this tab and return to your terminal.</p></body>`;
|
|
25259
|
-
PAGE_FAIL2 = `<!doctype html><meta charset="utf-8"><title>Sign-in failed</title>
|
|
25260
|
-
<body style="font:16px system-ui;padding:3rem;text-align:center">
|
|
25261
|
-
<h1>Sign-in failed</h1><p>Return to your terminal for details.</p></body>`;
|
|
25262
|
-
}
|
|
25263
|
-
});
|
|
25264
|
-
|
|
25265
|
-
// src/auth/cli.ts
|
|
25266
|
-
var cli_exports = {};
|
|
25267
|
-
__export(cli_exports, {
|
|
25268
|
-
exchangeCode: () => exchangeCode2,
|
|
25269
|
-
oauthConfigFromEnv: () => oauthConfigFromEnv,
|
|
25270
|
-
openBrowser: () => openBrowser,
|
|
25271
|
-
runAuthStatus: () => runAuthStatus,
|
|
25272
|
-
runLogin: () => runLogin,
|
|
25273
|
-
runLogout: () => runLogout
|
|
25274
|
-
});
|
|
25275
|
-
import { spawn as spawn6 } from "node:child_process";
|
|
25276
|
-
function oauthConfigFromEnv(env = process.env) {
|
|
25277
|
-
const issuer = env.CORENEL_OAUTH_ISSUER?.trim();
|
|
25278
|
-
const clientId = env.CORENEL_OAUTH_CLIENT_ID?.trim();
|
|
25279
|
-
if (!issuer || !clientId) return null;
|
|
25280
|
-
return { issuer, clientId };
|
|
25281
|
-
}
|
|
25282
|
-
function openBrowser(url) {
|
|
25283
|
-
const [cmd, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
25284
|
-
const child = spawn6(cmd, args, { detached: true, stdio: "ignore" });
|
|
25285
|
-
child.on("error", () => {
|
|
25286
|
-
});
|
|
25287
|
-
child.unref();
|
|
25288
|
-
}
|
|
25289
|
-
async function exchangeCode2(p2) {
|
|
25290
|
-
const res = await fetch(new URL("/oauth/token", p2.issuer), {
|
|
25291
|
-
method: "POST",
|
|
25292
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
25293
|
-
body: new URLSearchParams({
|
|
25294
|
-
grant_type: "authorization_code",
|
|
25295
|
-
client_id: p2.clientId,
|
|
25296
|
-
code: p2.code,
|
|
25297
|
-
code_verifier: p2.verifier,
|
|
25298
|
-
redirect_uri: p2.redirectUri
|
|
25299
|
-
}).toString()
|
|
25300
|
-
});
|
|
25301
|
-
if (!res.ok) {
|
|
25302
|
-
throw new Error(`token exchange failed (HTTP ${res.status})`);
|
|
25303
|
-
}
|
|
25304
|
-
return await res.json();
|
|
25305
|
-
}
|
|
25306
|
-
async function runLogin(stateDir, apiBase2) {
|
|
25307
|
-
const config = oauthConfigFromEnv();
|
|
25308
|
-
if (!config) {
|
|
25309
|
-
console.error("Sign-in is not configured on this build.");
|
|
25310
|
-
console.error("Set CORENEL_OAUTH_ISSUER and CORENEL_OAUTH_CLIENT_ID, or use " + TOKEN_ENV2 + " for headless use.");
|
|
25311
|
-
return 1;
|
|
25312
|
-
}
|
|
25313
|
-
try {
|
|
25314
|
-
const res = await login({
|
|
25315
|
-
config,
|
|
25316
|
-
openBrowser,
|
|
25317
|
-
exchange: exchangeCode2,
|
|
25318
|
-
log: (l2) => console.error(l2),
|
|
25319
|
-
...stateDir ? { stateDir } : {},
|
|
25320
|
-
// Recorded so the daemon does not have to guess later which API this
|
|
25321
|
-
// credential belongs to -- see LoginDeps.apiBase.
|
|
25322
|
-
...apiBase2 ? { apiBase: apiBase2 } : {}
|
|
25323
|
-
});
|
|
25324
|
-
console.error(`Signed in${res.account ? ` as ${res.account}` : ""}.`);
|
|
25325
|
-
return 0;
|
|
25326
|
-
} catch (err) {
|
|
25327
|
-
console.error(`Sign-in failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
25328
|
-
return 1;
|
|
25329
|
-
}
|
|
25330
|
-
}
|
|
25331
|
-
async function runLogout(stateDir, env = process.env) {
|
|
25332
|
-
const before = await resolveCredential(env, stateDir);
|
|
25333
|
-
await clearCredential(stateDir);
|
|
25334
|
-
console.error("Signed out. (Paired devices are unaffected - run `hosts forget` to unpair.)");
|
|
25335
|
-
console.error(` Cleared ${tokenFilePath(stateDir)} and ${credentialFilePath(stateDir)}`);
|
|
25336
|
-
if (before.source === "none") console.error(" Nothing was stored - this machine was already signed out.");
|
|
25337
|
-
if (env[TOKEN_ENV2]?.trim()) {
|
|
25338
|
-
console.error(` NOTE: ${TOKEN_ENV2} is still set in this environment, so this machine is STILL signed in.`);
|
|
25339
|
-
}
|
|
25340
|
-
return 0;
|
|
25341
|
-
}
|
|
25342
|
-
function sourceLabel(source) {
|
|
25343
|
-
return source === "env" ? `${TOKEN_ENV2} environment variable` : source === "token-file" ? "token file" : source === "credential-file" ? "credential file (auth.json)" : "none";
|
|
25344
|
-
}
|
|
25345
|
-
async function runAuthStatus(stateDir, env = process.env) {
|
|
25346
|
-
const r2 = await resolveCredential(env, stateDir);
|
|
25347
|
-
if (!r2.token) {
|
|
25348
|
-
console.error("Not signed in. Run `corenel login`, or set " + TOKEN_ENV2 + ".");
|
|
25349
|
-
console.error(` Looked in ${tokenFilePath(stateDir)}`);
|
|
25350
|
-
console.error(` Looked in ${credentialFilePath(stateDir)}`);
|
|
25351
|
-
return 1;
|
|
25352
|
-
}
|
|
25353
|
-
console.error(`Signed in (${r2.source === "env" ? sourceLabel(r2.source) : r2.path}).`);
|
|
25354
|
-
console.error(` account ${r2.account ?? "not recorded in this store - run `corenel login` to record it"}`);
|
|
25355
|
-
console.error(` token ${maskToken(r2.token)}`);
|
|
25356
|
-
console.error(` source ${sourceLabel(r2.source)}`);
|
|
25357
|
-
console.error(` expiry ${r2.expired ? `EXPIRED at ${new Date(r2.expiresAt ?? 0).toISOString()} - run login again` : r2.expiresAt ? new Date(r2.expiresAt).toISOString() : "not recorded in this store"}`);
|
|
25358
|
-
return r2.expired ? 1 : 0;
|
|
25359
|
-
}
|
|
25360
|
-
var init_cli = __esm({
|
|
25361
|
-
"src/auth/cli.ts"() {
|
|
25362
|
-
"use strict";
|
|
25363
|
-
init_login();
|
|
25364
|
-
init_store();
|
|
25365
|
-
init_tools_node();
|
|
25366
|
-
}
|
|
25367
|
-
});
|
|
25368
|
-
|
|
25369
25420
|
// src/cli.ts
|
|
25370
25421
|
init_protocol();
|
|
25371
25422
|
import { join as join21 } from "node:path";
|
|
@@ -28083,7 +28134,127 @@ function makeFileMirrorSource(deps) {
|
|
|
28083
28134
|
|
|
28084
28135
|
// src/controlPlane.ts
|
|
28085
28136
|
init_protocol();
|
|
28086
|
-
|
|
28137
|
+
|
|
28138
|
+
// ../config/src/daemonConfig.ts
|
|
28139
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
28140
|
+
import { dirname, join as join3 } from "node:path";
|
|
28141
|
+
var DAEMON_CONFIG_VERSION = 1;
|
|
28142
|
+
var SECRET_KEYS = ["acpAuth", "tunnelToken", "token"];
|
|
28143
|
+
var KNOWN_KEYS = [
|
|
28144
|
+
"version",
|
|
28145
|
+
"port",
|
|
28146
|
+
"host",
|
|
28147
|
+
"roots",
|
|
28148
|
+
"addUnder",
|
|
28149
|
+
"cloneUnder",
|
|
28150
|
+
"allowLan",
|
|
28151
|
+
"allowOrigins",
|
|
28152
|
+
"tlsCert",
|
|
28153
|
+
"tlsKey",
|
|
28154
|
+
"selfSigned",
|
|
28155
|
+
"tunnel",
|
|
28156
|
+
"tunnelName",
|
|
28157
|
+
"tunnelHostname",
|
|
28158
|
+
"endpoint",
|
|
28159
|
+
"allowShell",
|
|
28160
|
+
"keepTranscripts",
|
|
28161
|
+
"isolation",
|
|
28162
|
+
"msbBin",
|
|
28163
|
+
"guestImage",
|
|
28164
|
+
"maxChainDepth",
|
|
28165
|
+
"name",
|
|
28166
|
+
"appUrl",
|
|
28167
|
+
"defaultModel",
|
|
28168
|
+
"mcpServers",
|
|
28169
|
+
"mcpAllow"
|
|
28170
|
+
];
|
|
28171
|
+
function daemonConfigPath(stateDir) {
|
|
28172
|
+
return join3(stateDir, "daemon.json");
|
|
28173
|
+
}
|
|
28174
|
+
async function readDaemonConfig(stateDir) {
|
|
28175
|
+
const empty = (corrupt) => ({ config: { version: DAEMON_CONFIG_VERSION }, unknown: {}, corrupt });
|
|
28176
|
+
let raw;
|
|
28177
|
+
try {
|
|
28178
|
+
raw = await readFile(daemonConfigPath(stateDir), "utf8");
|
|
28179
|
+
} catch {
|
|
28180
|
+
return empty(false);
|
|
28181
|
+
}
|
|
28182
|
+
let parsed;
|
|
28183
|
+
try {
|
|
28184
|
+
parsed = JSON.parse(raw);
|
|
28185
|
+
} catch {
|
|
28186
|
+
return empty(true);
|
|
28187
|
+
}
|
|
28188
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return empty(true);
|
|
28189
|
+
const src2 = parsed;
|
|
28190
|
+
const config = { version: DAEMON_CONFIG_VERSION };
|
|
28191
|
+
const unknown = {};
|
|
28192
|
+
for (const [k2, v2] of Object.entries(src2)) {
|
|
28193
|
+
if (k2 === "version") continue;
|
|
28194
|
+
if (KNOWN_KEYS.includes(k2)) {
|
|
28195
|
+
config[k2] = v2;
|
|
28196
|
+
} else {
|
|
28197
|
+
unknown[k2] = v2;
|
|
28198
|
+
}
|
|
28199
|
+
}
|
|
28200
|
+
return { config, unknown, corrupt: false };
|
|
28201
|
+
}
|
|
28202
|
+
async function writeDaemonConfig(stateDir, next, unknown = {}) {
|
|
28203
|
+
const body2 = { ...unknown, ...next, version: DAEMON_CONFIG_VERSION };
|
|
28204
|
+
const path = daemonConfigPath(stateDir);
|
|
28205
|
+
await mkdir(dirname(path), { recursive: true });
|
|
28206
|
+
await writeFile(path, `${JSON.stringify(body2, null, 2)}
|
|
28207
|
+
`, "utf8");
|
|
28208
|
+
}
|
|
28209
|
+
|
|
28210
|
+
// ../config/src/credential.ts
|
|
28211
|
+
init_tools_node();
|
|
28212
|
+
import { promises as fs6 } from "node:fs";
|
|
28213
|
+
var TOKEN_ENV2 = TOKEN_ENV;
|
|
28214
|
+
function authDir(stateDir) {
|
|
28215
|
+
return credentialDir(stateDir);
|
|
28216
|
+
}
|
|
28217
|
+
function credentialPath(stateDir) {
|
|
28218
|
+
return credentialFilePath(stateDir);
|
|
28219
|
+
}
|
|
28220
|
+
var STORED_CREDENTIAL_KEYS = ["accessToken", "refreshToken", "expiresAt", "account", "apiBase"];
|
|
28221
|
+
async function readForeignAuthJson(path) {
|
|
28222
|
+
let raw;
|
|
28223
|
+
try {
|
|
28224
|
+
raw = await fs6.readFile(path, "utf8");
|
|
28225
|
+
} catch {
|
|
28226
|
+
return {};
|
|
28227
|
+
}
|
|
28228
|
+
try {
|
|
28229
|
+
const parsed = JSON.parse(raw);
|
|
28230
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
28231
|
+
const obj = { ...parsed };
|
|
28232
|
+
for (const k2 of STORED_CREDENTIAL_KEYS) delete obj[k2];
|
|
28233
|
+
return obj;
|
|
28234
|
+
} catch {
|
|
28235
|
+
return {};
|
|
28236
|
+
}
|
|
28237
|
+
}
|
|
28238
|
+
async function saveCredential(cred, stateDir) {
|
|
28239
|
+
const path = credentialPath(stateDir);
|
|
28240
|
+
await fs6.mkdir(authDir(stateDir), { recursive: true });
|
|
28241
|
+
const foreign = await readForeignAuthJson(path);
|
|
28242
|
+
const next = { ...foreign, ...cred };
|
|
28243
|
+
await fs6.writeFile(path, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
28244
|
+
try {
|
|
28245
|
+
await fs6.chmod(path, 384);
|
|
28246
|
+
} catch {
|
|
28247
|
+
}
|
|
28248
|
+
await writeAuthToken(cred.accessToken, stateDir);
|
|
28249
|
+
}
|
|
28250
|
+
async function clearCredential(stateDir) {
|
|
28251
|
+
await clearAllCredentials(stateDir);
|
|
28252
|
+
}
|
|
28253
|
+
async function resolveCredential(env = process.env, stateDir, now2 = Date.now) {
|
|
28254
|
+
return await resolveNodeCredential({ env, now: now2, ...stateDir ? { stateDir } : {} });
|
|
28255
|
+
}
|
|
28256
|
+
|
|
28257
|
+
// src/controlPlane.ts
|
|
28087
28258
|
function describeBackend(url, mirrors, cred, probe) {
|
|
28088
28259
|
const detail = probe ? {
|
|
28089
28260
|
reachable: probe.answered,
|
|
@@ -28143,8 +28314,8 @@ function makeControlPlane(deps) {
|
|
|
28143
28314
|
|
|
28144
28315
|
// src/tools.ts
|
|
28145
28316
|
import { promises as fs7 } from "node:fs";
|
|
28146
|
-
import { spawn } from "node:child_process";
|
|
28147
|
-
import { resolve as resolve2, relative as relative2, isAbsolute as isAbsolute2, sep as sep2, dirname as
|
|
28317
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
28318
|
+
import { resolve as resolve2, relative as relative2, isAbsolute as isAbsolute2, sep as sep2, dirname as dirname4 } from "node:path";
|
|
28148
28319
|
function withinRoot(root, abs) {
|
|
28149
28320
|
const rel = relative2(root, abs);
|
|
28150
28321
|
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel) && rel.split(sep2)[0] !== "..";
|
|
@@ -28172,7 +28343,7 @@ async function assertRealWithinRoot(roots, abs) {
|
|
|
28172
28343
|
return;
|
|
28173
28344
|
} catch (e2) {
|
|
28174
28345
|
if (e2 instanceof Error && e2.message.includes("via symlink")) throw e2;
|
|
28175
|
-
const parent =
|
|
28346
|
+
const parent = dirname4(probe);
|
|
28176
28347
|
if (parent === probe) throw new Error(`cannot resolve path under the sidecar roots: ${abs}`);
|
|
28177
28348
|
probe = parent;
|
|
28178
28349
|
}
|
|
@@ -28223,7 +28394,7 @@ function nodeFsTools(roots) {
|
|
|
28223
28394
|
mutates: true,
|
|
28224
28395
|
async run(args) {
|
|
28225
28396
|
const abs = confine(roots, args.root, String(args.path ?? ""));
|
|
28226
|
-
await fs7.mkdir(
|
|
28397
|
+
await fs7.mkdir(dirname4(abs), { recursive: true });
|
|
28227
28398
|
await assertRealWithinRoot(roots, abs);
|
|
28228
28399
|
await fs7.writeFile(abs, String(args.content ?? ""), "utf8");
|
|
28229
28400
|
return `wrote ${args.path}`;
|
|
@@ -28274,7 +28445,7 @@ function nodeFsTools(roots) {
|
|
|
28274
28445
|
if (bytes.byteLength > MAX_READ_BYTES) {
|
|
28275
28446
|
throw new Error(`file too large to write (${bytes.byteLength} bytes; max ${MAX_READ_BYTES})`);
|
|
28276
28447
|
}
|
|
28277
|
-
await fs7.mkdir(
|
|
28448
|
+
await fs7.mkdir(dirname4(abs), { recursive: true });
|
|
28278
28449
|
await assertRealWithinRoot(roots, abs);
|
|
28279
28450
|
await fs7.writeFile(abs, bytes);
|
|
28280
28451
|
return `wrote ${args.path} (${bytes.byteLength} bytes)`;
|
|
@@ -28389,7 +28560,7 @@ function runShell(command, cwd, timeout) {
|
|
|
28389
28560
|
return new Promise((resolveOut) => {
|
|
28390
28561
|
const isWin = process.platform === "win32";
|
|
28391
28562
|
const [shell, shellArgs] = isWin ? ["cmd.exe", ["/d", "/s", "/c", command]] : ["/bin/sh", ["-c", command]];
|
|
28392
|
-
const child =
|
|
28563
|
+
const child = spawn2(shell, shellArgs, { cwd, windowsHide: true });
|
|
28393
28564
|
let out = "";
|
|
28394
28565
|
let truncated = false;
|
|
28395
28566
|
let timedOut = false;
|
|
@@ -28424,7 +28595,7 @@ ${truncated ? "[output truncated]\n" : ""}${body2}`);
|
|
|
28424
28595
|
// src/configService.ts
|
|
28425
28596
|
init_protocol();
|
|
28426
28597
|
import { promises as fs8 } from "node:fs";
|
|
28427
|
-
import { resolve as resolve3, relative as relative3, join as
|
|
28598
|
+
import { resolve as resolve3, relative as relative3, join as join7, dirname as dirname5, sep as sep3 } from "node:path";
|
|
28428
28599
|
var MAX_LIST_DEPTH2 = 24;
|
|
28429
28600
|
var MAX_LIST_ENTRIES2 = 2e4;
|
|
28430
28601
|
function relUnder(root, abs) {
|
|
@@ -28459,7 +28630,7 @@ var NodeConfigService = class {
|
|
|
28459
28630
|
real = await fs8.realpath(probe);
|
|
28460
28631
|
} catch {
|
|
28461
28632
|
if (mustExist && probe === abs) throw new Error(`no such harness config file: ${relUnder(this.stateDir, abs) ?? abs}`);
|
|
28462
|
-
const parent =
|
|
28633
|
+
const parent = dirname5(probe);
|
|
28463
28634
|
if (parent === probe) throw new Error("cannot resolve a path inside the harness config");
|
|
28464
28635
|
probe = parent;
|
|
28465
28636
|
continue;
|
|
@@ -28495,11 +28666,11 @@ var NodeConfigService = class {
|
|
|
28495
28666
|
async listRoot(levels) {
|
|
28496
28667
|
const out = [];
|
|
28497
28668
|
for (const name of CONFIG_DEFINITION_FILES) {
|
|
28498
|
-
const st2 = await fs8.stat(
|
|
28669
|
+
const st2 = await fs8.stat(join7(this.stateDir, name)).catch(() => null);
|
|
28499
28670
|
if (st2?.isFile()) out.push({ path: name, size: st2.size, mtime: st2.mtimeMs });
|
|
28500
28671
|
}
|
|
28501
28672
|
for (const name of CONFIG_DEFINITION_DIRS) {
|
|
28502
|
-
const abs =
|
|
28673
|
+
const abs = join7(this.stateDir, name);
|
|
28503
28674
|
const st2 = await fs8.stat(abs).catch(() => null);
|
|
28504
28675
|
if (!st2?.isDirectory()) continue;
|
|
28505
28676
|
out.push({ path: name, dir: true });
|
|
@@ -28514,7 +28685,7 @@ var NodeConfigService = class {
|
|
|
28514
28685
|
for (const e2 of entries) {
|
|
28515
28686
|
if (out.length >= MAX_LIST_ENTRIES2) return;
|
|
28516
28687
|
if (e2.isSymbolicLink()) continue;
|
|
28517
|
-
const child =
|
|
28688
|
+
const child = join7(abs, e2.name);
|
|
28518
28689
|
if (e2.isDirectory()) {
|
|
28519
28690
|
out.push({ path: `${prefix}${e2.name}`, dir: true });
|
|
28520
28691
|
await this.walk(child, `${prefix}${e2.name}/`, levels - 1, out);
|
|
@@ -28533,7 +28704,7 @@ var NodeConfigService = class {
|
|
|
28533
28704
|
* partially written AGENT.md is a definition that parses to nonsense. */
|
|
28534
28705
|
async write(path, content) {
|
|
28535
28706
|
const abs = await this.confine(path, false);
|
|
28536
|
-
await fs8.mkdir(
|
|
28707
|
+
await fs8.mkdir(dirname5(abs), { recursive: true });
|
|
28537
28708
|
await this.assertRealAllowed(abs, false);
|
|
28538
28709
|
const tmp = `${abs}.${process.pid}.${Date.now()}.tmp`;
|
|
28539
28710
|
try {
|
|
@@ -28552,7 +28723,7 @@ var NodeConfigService = class {
|
|
|
28552
28723
|
};
|
|
28553
28724
|
|
|
28554
28725
|
// src/procs.ts
|
|
28555
|
-
import { spawn as
|
|
28726
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
28556
28727
|
var MAX_RUNNING = 8;
|
|
28557
28728
|
var MAX_CHUNKS = 400;
|
|
28558
28729
|
var MAX_CHARS = 2e5;
|
|
@@ -28614,7 +28785,7 @@ function createNodeProcs(opts) {
|
|
|
28614
28785
|
const isWin = process.platform === "win32";
|
|
28615
28786
|
const shell = isWin ? "cmd.exe" : "/bin/sh";
|
|
28616
28787
|
const shellArgs = isWin ? ["/d", "/s", "/c", line] : ["-c", line];
|
|
28617
|
-
const child =
|
|
28788
|
+
const child = spawn3(shell, shellArgs, {
|
|
28618
28789
|
cwd: dir,
|
|
28619
28790
|
windowsHide: true,
|
|
28620
28791
|
stdio: "pipe",
|
|
@@ -29153,7 +29324,11 @@ var STANDARD_POLICY = {
|
|
|
29153
29324
|
name: "standard",
|
|
29154
29325
|
description: "Read-only tools run automatically; writes ask for approval.",
|
|
29155
29326
|
onViolation: "stop",
|
|
29156
|
-
maxTurns
|
|
29327
|
+
// maxTurns counts iterations WITHIN one user message (one model call plus
|
|
29328
|
+
// its tool round-trip), not messages across a conversation -- 8 was
|
|
29329
|
+
// exhausted inside a single tool-heavy reply. 0 means unlimited (see
|
|
29330
|
+
// core/loop.ts); this default stays a finite backstop.
|
|
29331
|
+
maxTurns: 40,
|
|
29157
29332
|
permissions: {
|
|
29158
29333
|
"prompd:propose_edit": "allow",
|
|
29159
29334
|
// proposals are reviewed in the diff UI, not applied directly
|
|
@@ -29167,7 +29342,8 @@ var READ_ONLY_POLICY = {
|
|
|
29167
29342
|
name: "read-only",
|
|
29168
29343
|
description: "Inspection only \u2014 all writes and side effects are refused.",
|
|
29169
29344
|
onViolation: "stop",
|
|
29170
|
-
maxTurns:
|
|
29345
|
+
maxTurns: 40,
|
|
29346
|
+
// see STANDARD_POLICY: counts iterations within one reply, not conversation turns
|
|
29171
29347
|
tools: {
|
|
29172
29348
|
deny: [
|
|
29173
29349
|
"fs:write_file",
|
|
@@ -29210,7 +29386,9 @@ var AUTONOMOUS_POLICY = {
|
|
|
29210
29386
|
name: "autonomous",
|
|
29211
29387
|
description: "Everything runs automatically \u2014 no approval prompts (system config still asks).",
|
|
29212
29388
|
onViolation: "warn",
|
|
29213
|
-
|
|
29389
|
+
// Higher than the other builtins: nobody is watching an autonomous run turn
|
|
29390
|
+
// by turn, so it gets more headroom before the backstop kicks in.
|
|
29391
|
+
maxTurns: 80,
|
|
29214
29392
|
permissions: {
|
|
29215
29393
|
"system:*": "ask"
|
|
29216
29394
|
// even unattended, never silently reconfigure the harness
|
|
@@ -29552,6 +29730,96 @@ var todoWriteTool = {
|
|
|
29552
29730
|
}
|
|
29553
29731
|
};
|
|
29554
29732
|
|
|
29733
|
+
// ../harness/tools/project.ts
|
|
29734
|
+
init_protocol();
|
|
29735
|
+
var manifestSuffix = () => `${stateDirName()}/craft/project.json`;
|
|
29736
|
+
async function findProjects(ctx) {
|
|
29737
|
+
const files = ctx.files;
|
|
29738
|
+
if (!files) return [];
|
|
29739
|
+
const suffix = manifestSuffix();
|
|
29740
|
+
const nodes = await files.list().catch(() => []);
|
|
29741
|
+
const out = [];
|
|
29742
|
+
for (const node of flattenFiles(nodes)) {
|
|
29743
|
+
const p2 = node.path;
|
|
29744
|
+
const root = p2 === suffix ? "" : p2.endsWith(`/${suffix}`) ? p2.slice(0, -(suffix.length + 1)) : null;
|
|
29745
|
+
if (root === null) continue;
|
|
29746
|
+
try {
|
|
29747
|
+
const m2 = JSON.parse(await files.read(p2));
|
|
29748
|
+
if (m2 && m2.version === 1 && typeof m2.name === "string") out.push({ root, name: m2.name });
|
|
29749
|
+
} catch {
|
|
29750
|
+
}
|
|
29751
|
+
}
|
|
29752
|
+
return out;
|
|
29753
|
+
}
|
|
29754
|
+
function projectSlug(name) {
|
|
29755
|
+
const s2 = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
29756
|
+
return s2 || "project";
|
|
29757
|
+
}
|
|
29758
|
+
async function freeRoot(ctx, name) {
|
|
29759
|
+
const files = ctx.files;
|
|
29760
|
+
const base = projectSlug(name);
|
|
29761
|
+
let root = base;
|
|
29762
|
+
for (let i2 = 2; files && await files.stat(root) !== null; i2++) root = `${base}-${i2}`;
|
|
29763
|
+
return root;
|
|
29764
|
+
}
|
|
29765
|
+
var startProjectTool = {
|
|
29766
|
+
name: "start_project",
|
|
29767
|
+
/* DECLARED, like ask_user's, and for the same reason: its whole effect is to
|
|
29768
|
+
* put a choice in front of someone who is already there, and the write it
|
|
29769
|
+
* may then make is the thing they just chose. A permission prompt on top of
|
|
29770
|
+
* the choice is two dialogs for one decision. */
|
|
29771
|
+
permission: "allow",
|
|
29772
|
+
noOffload: true,
|
|
29773
|
+
description: "Use when the person asks for a DOCUMENT (a memo, report, deck, spreadsheet, diagram, page) and the system context says no project is open. Documents live inside a project, where they are versioned and shown beside the conversation; a file written outside one is invisible in this surface. Propose a short project name derived from what they asked for \u2014 the person confirms it, picks an existing project instead, or declines. This call WAITS for their answer and returns the folder to write into, or that they declined. Write the document inside that folder with the ordinary file tools. Do NOT use this for ordinary code or repository files, which belong where they already are.",
|
|
29774
|
+
parameters: {
|
|
29775
|
+
type: "object",
|
|
29776
|
+
properties: {
|
|
29777
|
+
name: { type: "string", description: "Proposed project name, 2-4 words, derived from the request." },
|
|
29778
|
+
reason: { type: "string", description: "One short line: what is about to be written into it." }
|
|
29779
|
+
},
|
|
29780
|
+
required: ["name"]
|
|
29781
|
+
},
|
|
29782
|
+
async run(args, ctx) {
|
|
29783
|
+
const files = ctx.files;
|
|
29784
|
+
if (!files) return "start_project is unavailable: no file workspace is connected.";
|
|
29785
|
+
if (!ctx.ask) return "start_project is unavailable in this environment (nobody can be asked).";
|
|
29786
|
+
if (!files.writable) return "This workspace is read-only, so no project can be created in it.";
|
|
29787
|
+
const proposed = (typeof args.name === "string" ? args.name : "").trim() || "Untitled project";
|
|
29788
|
+
const existing = await findProjects(ctx);
|
|
29789
|
+
const createLabel = `Create "${proposed}"`;
|
|
29790
|
+
const options = [
|
|
29791
|
+
{ label: createLabel, description: typeof args.reason === "string" && args.reason ? args.reason : "A new project for this work." },
|
|
29792
|
+
...existing.slice(0, 3).map((p2) => ({ label: p2.name, description: `Put it in the existing project at ${p2.root || "the workspace root"}.` }))
|
|
29793
|
+
];
|
|
29794
|
+
if (options.length < 2) options.push({ label: "Not now", description: "Write nothing; the document has nowhere to live yet." });
|
|
29795
|
+
const canMove = !!ctx.moveSessionToProject;
|
|
29796
|
+
const questions = [
|
|
29797
|
+
{ header: "Project", question: "Documents live in a project. Where should this one go?", options },
|
|
29798
|
+
...canMove ? [{
|
|
29799
|
+
header: "This chat",
|
|
29800
|
+
question: "Bring this conversation into the project too?",
|
|
29801
|
+
options: [
|
|
29802
|
+
{ label: "Yes, move it", description: "The transcript moves with the work, and this chat carries on." },
|
|
29803
|
+
{ label: "Keep it loose", description: "The document goes in the project; this chat stays where it is." }
|
|
29804
|
+
]
|
|
29805
|
+
}] : []
|
|
29806
|
+
];
|
|
29807
|
+
const answers = await ctx.ask(questions);
|
|
29808
|
+
const picked = answers ? answers.Project?.[0] : void 0;
|
|
29809
|
+
const move = answers ? answers["This chat"]?.[0] === "Yes, move it" : false;
|
|
29810
|
+
if (!picked) return "The person did not choose a project. Do not write the document; say what you were about to write and that it needs a project.";
|
|
29811
|
+
if (picked === "Not now") return "The person declined to start a project. Do not write the document.";
|
|
29812
|
+
const moved = async (root2) => move && ctx.moveSessionToProject ? ` ${await ctx.moveSessionToProject(root2)}` : "";
|
|
29813
|
+
const hit = existing.find((p2) => p2.name === picked);
|
|
29814
|
+
if (hit) return `Use the existing project "${hit.name}". Write the document inside ${hit.root || "the workspace root"}/.${await moved(hit.root)}`;
|
|
29815
|
+
const name = picked === createLabel ? proposed : picked;
|
|
29816
|
+
const root = await freeRoot(ctx, name);
|
|
29817
|
+
const manifest = { name, createdAt: Date.now(), version: 1 };
|
|
29818
|
+
await files.write(`${root}/${manifestSuffix()}`, JSON.stringify(manifest, null, 2));
|
|
29819
|
+
return `Created the project "${name}" at ${root}/. Write the document inside ${root}/ with the ordinary file tools; it will be versioned and shown beside this conversation.${await moved(root)}`;
|
|
29820
|
+
}
|
|
29821
|
+
};
|
|
29822
|
+
|
|
29555
29823
|
// ../harness/tools/sessions.ts
|
|
29556
29824
|
function str3(args, key) {
|
|
29557
29825
|
const v2 = args[key];
|
|
@@ -29853,6 +30121,10 @@ var forgetMemoryTool = {
|
|
|
29853
30121
|
return `Forgot memory "${name}".`;
|
|
29854
30122
|
}
|
|
29855
30123
|
};
|
|
30124
|
+
function preview(v2, max = 200) {
|
|
30125
|
+
if (v2.length <= max) return JSON.stringify(v2);
|
|
30126
|
+
return `${JSON.stringify(v2.slice(0, max))}... (${v2.length} chars total)`;
|
|
30127
|
+
}
|
|
29856
30128
|
var kvGetTool = {
|
|
29857
30129
|
name: "kv_get",
|
|
29858
30130
|
description: "Read a value you previously saved with kv_set, by key, from the local key/value store. This is a general note store \u2014 NOT a filesystem, working directory, or settings: use list_files/read_file for files.",
|
|
@@ -29872,10 +30144,42 @@ var kvSetTool = {
|
|
|
29872
30144
|
required: ["key", "value"]
|
|
29873
30145
|
},
|
|
29874
30146
|
// Permission is gated centrally by the loop's policy/permission layer.
|
|
30147
|
+
/*
|
|
30148
|
+
* THE RESULT STATES WHAT WAS STORED, AND WHAT IT REPLACED.
|
|
30149
|
+
*
|
|
30150
|
+
* This used to return `Set ${key}.` -- nothing else. Two consequences, both
|
|
30151
|
+
* observed in a real run:
|
|
30152
|
+
*
|
|
30153
|
+
* 1. "set bread=true" and "set bread=false" produced BYTE-IDENTICAL tool
|
|
30154
|
+
* results. An agent had no feedback that could distinguish a correct
|
|
30155
|
+
* write from a wrong one, so a mistaken value was indistinguishable from
|
|
30156
|
+
* a correct one for the rest of the conversation.
|
|
30157
|
+
*
|
|
30158
|
+
* 2. A LOST UPDATE was invisible. The common shape here is a list kept as
|
|
30159
|
+
* one blob under one key, updated read-modify-write: a later write
|
|
30160
|
+
* carrying a stale copy silently reverts an earlier one, and every
|
|
30161
|
+
* result still says "Set". A run marked two items done and only one
|
|
30162
|
+
* stayed done, with nothing anywhere to show the second had been undone.
|
|
30163
|
+
*
|
|
30164
|
+
* So: report the value, and name the value being replaced. An agent that
|
|
30165
|
+
* overwrites "apples done, bread done" with "apples done, bread pending"
|
|
30166
|
+
* now reads back, in the tool result it already has to process, exactly
|
|
30167
|
+
* what it just destroyed. The read-back is what makes this a check rather
|
|
30168
|
+
* than an echo of the argument -- a store that transformed, truncated or
|
|
30169
|
+
* dropped the write is caught here instead of at the summary.
|
|
30170
|
+
*/
|
|
29875
30171
|
async run(args, ctx) {
|
|
29876
30172
|
const key = str4(args, "key");
|
|
29877
|
-
|
|
29878
|
-
|
|
30173
|
+
const value = str4(args, "value");
|
|
30174
|
+
const before = await ctx.kv.get(key);
|
|
30175
|
+
await ctx.kv.set(key, value);
|
|
30176
|
+
const after = await ctx.kv.get(key);
|
|
30177
|
+
if (after !== value) {
|
|
30178
|
+
return `WRITE DID NOT TAKE. "${key}" was set to ${preview(value)} but reads back as ${after == null ? "(not set)" : preview(after)}. Do not rely on this key; do not report it as saved.`;
|
|
30179
|
+
}
|
|
30180
|
+
if (before == null) return `Set "${key}" = ${preview(value)} (new key).`;
|
|
30181
|
+
if (before === value) return `Set "${key}" = ${preview(value)} (unchanged).`;
|
|
30182
|
+
return `Set "${key}" = ${preview(value)}, REPLACING ${preview(before)}. If that previous value held anything you did not mean to discard, it is gone.`;
|
|
29879
30183
|
}
|
|
29880
30184
|
};
|
|
29881
30185
|
var recallResultTool = {
|
|
@@ -30133,6 +30437,7 @@ var defaultTools = [
|
|
|
30133
30437
|
webSearchTool,
|
|
30134
30438
|
todoWriteTool,
|
|
30135
30439
|
compilePrompdTool,
|
|
30440
|
+
startProjectTool,
|
|
30136
30441
|
...fileTools
|
|
30137
30442
|
];
|
|
30138
30443
|
function allTools() {
|
|
@@ -30212,10 +30517,10 @@ Add a constraint :: Add a constraint or rule to the prompt instructions (what to
|
|
|
30212
30517
|
`,
|
|
30213
30518
|
"guards.json": '[\n {\n "_comment": "Declarative client-side feature guards (NON-authoritative \u2014 for defaults/UX; real enforcement is the backend entitlements API). Copy an entry and remove \\"disabled\\". rule: \\"require-auth\\" or \\"free-quota\\" (with \\"limit\\"). feature: a FeatureId like \\"llm-execution\\", or \\"*\\". action: { \\"kind\\": \\"sign-in\\" } or { \\"kind\\": \\"upgrade\\", \\"label\\": \\"...\\" }.",\n "disabled": true,\n "id": "example-free-quota",\n "feature": "llm-execution",\n "rule": "free-quota",\n "limit": 3,\n "reason": "Free limit reached \u2014 sign in to keep going.",\n "action": { "kind": "sign-in" }\n }\n]\n',
|
|
30214
30519
|
"languages/prmd.md": '{% raw %}\n# PRMD File Format Overview\n\nPRMD files are structured in YAML format and are used to define prompts for various applications within the Prompd system. Each file contains essential metadata in the frontmatter followed by the body content, which is where the prompt logic and structure reside.\n\n## Field Descriptions\n- **id**: A unique identifier for the prompt file, usually in kebab-case.\n- **name**: A brief title for the prompt that identifies it.\n- **version**: The version number of the prompt file, following semantic versioning.\n- **description**: Optional human-readable summary.\n- **model** / **provider**: Optional default model + provider for runs.\n- **inherits**: A path to another prompt file whose body this one extends (the parent renders first, then this file\'s body). Resolved at compile time \u2014 siblings locally, registry refs on the backend.\n- **using**: Optional list of package refs imported for context/blocks; each `{ name: "@scope/pkg@1.2.0", prefix: "@alias" }`.\n- **parameters**: A list of typed input variables for the prompt (see below).\n\n## Parameter Types\nPrompd supports thirteen parameter types covering scalars, dates, structured data, and binary content.\n\n| Type | Description |\n|----------|-----------------------------------------------------------------|\n| string | Plain text |\n| enum | A value restricted to a fixed set of options (see Enums below) |\n| number | Integer or float (generic numeric) |\n| integer | Whole number only \u2014 rejects floats |\n| float | Decimal number |\n| boolean | true / false |\n| date | Calendar date \u2192 `YYYY-MM-DD`. Resolved at compile time. |\n| datetime | Date + time \u2192 `YYYY-MM-DDTHH:mm:ss` (local). Resolved at compile time. |\n| array | JSON array with untyped elements |\n| object | Plain key-value object (non-array) |\n| json | Any JSON value \u2014 arrays of objects, nested structures, etc. |\n| file | File path; caller supplies the file content as a string |\n| base64 | Base64-encoded binary (images, PDFs, blobs for multimodal LLMs) |\n\n### Enums (restricted values)\nAn `enum` parameter limits the value to a fixed set of choices. Provide the allowed values as a JSON array of strings in an `enum:` field, e.g. `["small", "medium", "large"]` (a YAML block list works too). Set `type: enum` \u2014 or omit `type`, since it\'s inferred when `enum` is present. The `default:`, if given, must be one of the options.\n\n```yaml\n- name: tone\n type: enum\n enum: ["detailed", "summary", "concise"]\n default: "summary"\n```\n\n### Date & datetime defaults\n`date` and `datetime` defaults accept `now` / `today`, a relative offset `now\xB1N<unit>`, or a literal ISO date. Units: `d` (days), `w` (weeks), `mo` (months), `y` (years); `datetime` also `h` (hours), `min`, `s`. The value is resolved at compile time, so each run uses the current clock.\n\nExamples (assume today is 2026-06-11):\n\n| Default | Type | Resolves to |\n|----------------|----------|------------------------|\n| `now` | date | `2026-06-11` |\n| `now-8d` | date | `2026-06-03` |\n| `now-2w` | date | `2026-05-28` |\n| `now-1mo` | date | `2026-05-11` |\n| `now+1y` | date | `2027-06-11` |\n| `now` | datetime | `2026-06-11T09:30:00` |\n| `now-3h` | datetime | `2026-06-11T06:30:00` |\n| `2025-01-15` | date | `2025-01-15` |\n\n## Body (Markdown + Jinja2/Nunjucks)\n- `{{ param }}` and `{{ param | filter }}` interpolation.\n- `{% if %}` / `{% elif %}` / `{% else %}` / `{% endif %}` conditionals.\n- `{% for x in list %} \u2026 {% endfor %}` loops.\n- `{% include %}` and inherited blocks resolve against the workspace + registry at compile time.\n- Only declared parameters are guaranteed to exist; treat any other variable as undefined.\n\n## Example PRMD File\n```yaml\n---\nid: report-window\nname: Report Window\nversion: 1.0.0\nparameters:\n - name: user_name\n type: string\n default: "Guest"\n - name: since\n type: date\n default: "now-8d" # 8 days ago, resolved at compile time\n - name: generated_at\n type: datetime\n default: "now"\n---\nHello, {{ user_name }}! Here is your report covering everything since {{ since }} (generated {{ generated_at }}).\n```\n{% endraw %}\n',
|
|
30215
|
-
"modes/auto.md": "---\nid: auto\nname: Auto\ndescription:
|
|
30216
|
-
"modes/brainstorm.md": "---\nid: brainstorm\nname: Brainstorm\ndescription:
|
|
30217
|
-
"modes/edit.md": "---\nid: edit\nname: Edit\ndescription:
|
|
30218
|
-
"modes/plan.md": "---\nid: plan\nname: Plan\ndescription:
|
|
30520
|
+
"modes/auto.md": "---\nid: auto\nname: Auto\ndescription: Act on the request, deciding what is cheap to decide and asking only what is expensive to get wrong.\n---\nMode: AUTO \u2014 act.\n\nMake the smallest set of decisions needed to start, say plainly what you assumed, and build a thin working slice before elaborating it. A broad request is not a reason to stop and interview the user.\n\nAsk only where a wrong guess is expensive to undo \u2014 the data model, anything touching security, privacy or regulated data, and where the thing runs. Keep it to two or three questions and ask them before you build, not after. Everything that is cheap to change later is an assumption to state, not a question to ask.\n",
|
|
30521
|
+
"modes/brainstorm.md": "---\nid: brainstorm\nname: Brainstorm\ndescription: Generate genuinely different options with their trade-offs, and recommend one.\n---\nMode: BRAINSTORM \u2014 explore options and alternatives; be generative.\n\nGive at least two genuinely different approaches, not one approach with its dial turned. Name what each is good at and what it costs, then say which you would pick and why. Do not edit files.\n",
|
|
30522
|
+
"modes/edit.md": "---\nid: edit\nname: Edit\ndescription: Make the smallest concrete change that does the job.\n---\nMode: EDIT \u2014 make the smallest concrete change that does the job, and say what you changed.\n\nWhen a file is open in the editor, prefer propose_edit with the COMPLETE new file content, so the user reviews a diff before anything is applied.\n",
|
|
30523
|
+
"modes/plan.md": "---\nid: plan\nname: Plan\ndescription: Settle the shape-changing decisions first, then outline the approach without mutating anything.\n---\nMode: PLAN \u2014 decide before acting.\n\nFirst surface the decisions that change the SHAPE of the result and ask about those; an outline built on an unasked question is wasted work. Then think step by step and lay out the approach, with the trade-offs you are choosing between made explicit.\n\nAvoid mutating tools. Producing a plan the user can argue with is the deliverable here.\n",
|
|
30219
30524
|
"crew/memory.md": '---\nid: crew-memory\nname: Crew agent \u2014 memory block\nversion: 1.0.0\ndescription: Told to every crew agent about its own memory folder. Appended after the agent\'s own role and instructions, because it is housekeeping and they are the job.\nparameters:\n - name: dir\n type: string\n description: The agent\'s memory folder, with no trailing slash (e.g. crew/auditor/memory).\n default: "memory"\n - name: index\n type: string\n description: Full path of the agent\'s memory index, derived from dir (e.g. crew/auditor/memory/MEMORY.md).\n default: "memory/MEMORY.md"\n---\n--- Memory ---\nYou keep durable notes in your own memory folder, `{{ dir }}/`, one Markdown file per note, alongside `{{ index }}`, an index you maintain for yourself.\nRead `{{ index }}` at the start of a run to recall what you already know, and call recall_memory to look up one note by name. If you can call save_memory, use it for what is worth knowing on a later run, and keep `{{ index }}` current as you do. If a file tool you have takes a `root` argument, you can pass `{{ dir }}/` as `root` to read and edit those files directly.\n',
|
|
30220
30525
|
"crew/room.md": `---
|
|
30221
30526
|
id: crew-room
|
|
@@ -30350,7 +30655,7 @@ You are a privacy reviewer. Determine whether the INPUT exposes personally ident
|
|
|
30350
30655
|
id: system-base
|
|
30351
30656
|
name: Operational system prompt
|
|
30352
30657
|
version: 1.0.0
|
|
30353
|
-
description: The agent's system prompt \u2014
|
|
30658
|
+
description: The agent's system prompt \u2014 surface-agnostic identity, mode, tool definitions and context composed by @prompd/core. Editor and workflow specifics live in the prompts that inherit this one.
|
|
30354
30659
|
parameters:
|
|
30355
30660
|
- name: persona
|
|
30356
30661
|
type: string
|
|
@@ -30376,6 +30681,18 @@ parameters:
|
|
|
30376
30681
|
type: string
|
|
30377
30682
|
description: The editor file / selection / compiled-output context block.
|
|
30378
30683
|
default: ""
|
|
30684
|
+
- name: skills
|
|
30685
|
+
type: string
|
|
30686
|
+
description: Instructions contributed by the skills enabled for this run.
|
|
30687
|
+
default: ""
|
|
30688
|
+
- name: memory
|
|
30689
|
+
type: string
|
|
30690
|
+
description: The index of notes saved to this run's memory plane.
|
|
30691
|
+
default: ""
|
|
30692
|
+
- name: run_context
|
|
30693
|
+
type: string
|
|
30694
|
+
description: Facts about THIS run the model cannot otherwise know \u2014 the date, and whatever the host adds (working directory, platform).
|
|
30695
|
+
default: ""
|
|
30379
30696
|
- name: app
|
|
30380
30697
|
type: string
|
|
30381
30698
|
description: Product name of the host application, used for the agent's own identity.
|
|
@@ -30383,11 +30700,22 @@ parameters:
|
|
|
30383
30700
|
---
|
|
30384
30701
|
{% if persona %}{{ persona }}
|
|
30385
30702
|
|
|
30386
|
-
{% endif %}You are the {{ app }} assistant
|
|
30703
|
+
{% endif %}You are the {{ app }} assistant. You help the user do real work: reading and changing files, composing and running prompts and workflows, and building software. Work from the context you were given below rather than assuming a particular screen.
|
|
30387
30704
|
|
|
30388
|
-
|
|
30705
|
+
Verify before you claim. Do not say something is done, fixed, passing or ready unless you checked it in this conversation and can point at what you checked. If you did not run it, say you did not run it. If a check failed, say so plainly and show what failed. A wrong claim of success costs the user far more than the check would have.
|
|
30389
30706
|
|
|
30390
|
-
|
|
30707
|
+
Say what you assumed and what you did not do. A request that leaves room for interpretation gets its decisions named, one line each, so the user can correct one without unpicking the rest. How much you decide for yourself versus ask about is the mode's call, below.
|
|
30708
|
+
|
|
30709
|
+
A tool call that comes back denied, blocked or unavailable is a decision, not a failure to route around. Do not retry it, do not reach for another tool to accomplish the same thing, and never describe the change as made. Say what you wanted to do and stop.
|
|
30710
|
+
|
|
30711
|
+
Prefer calling a tool over guessing: read the file rather than recalling it, run the command rather than predicting its output. Use the file tools to read and change files, and compile_prompd to preview rendered output. For a job with several steps, keep a visible task list with todo_write and mark each step as you finish it.
|
|
30712
|
+
|
|
30713
|
+
Be brief. Lead with the answer, then only what the user needs to act on or check it. Use headings, lists and tables where they carry something prose would not, and prose everywhere else. Do not restate the request back, do not narrate what you are about to do, and do not end by summarizing what the user has just read.
|
|
30714
|
+
|
|
30715
|
+
{% if run_context %}## This run
|
|
30716
|
+
{{ run_context }}
|
|
30717
|
+
|
|
30718
|
+
{% endif %}## Tools available this run
|
|
30391
30719
|
{{ tools }}
|
|
30392
30720
|
{% if tool_guidance %}
|
|
30393
30721
|
|
|
@@ -30398,6 +30726,14 @@ To change the file open in the editor, call propose_edit with the COMPLETE new f
|
|
|
30398
30726
|
## Mode
|
|
30399
30727
|
{{ mode_hint }}
|
|
30400
30728
|
{% endif %}
|
|
30729
|
+
{% if skills %}
|
|
30730
|
+
|
|
30731
|
+
{{ skills }}
|
|
30732
|
+
{% endif %}
|
|
30733
|
+
{% if memory %}
|
|
30734
|
+
|
|
30735
|
+
{{ memory }}
|
|
30736
|
+
{% endif %}
|
|
30401
30737
|
{% if file_context %}
|
|
30402
30738
|
|
|
30403
30739
|
{{ file_context }}
|
|
@@ -32577,6 +32913,8 @@ function makeCrewTurnRuntime(opts) {
|
|
|
32577
32913
|
policy,
|
|
32578
32914
|
permission,
|
|
32579
32915
|
...deps.maxTurns !== void 0 ? { maxTurns: deps.maxTurns } : {},
|
|
32916
|
+
...deps.compression ? { compression: deps.compression } : {},
|
|
32917
|
+
...deps.compaction ? { compaction: deps.compaction } : {},
|
|
32580
32918
|
...deps.temperature !== void 0 ? { temperature: deps.temperature } : {},
|
|
32581
32919
|
...deps.describePermissionDenial ? { describePermissionDenial: deps.describePermissionDenial } : {},
|
|
32582
32920
|
signal: mergeSignals(deps.toolCtx.signal, turnAbort.signal),
|
|
@@ -32592,7 +32930,7 @@ function makeCrewTurnRuntime(opts) {
|
|
|
32592
32930
|
deps.onEvent?.(e2);
|
|
32593
32931
|
}
|
|
32594
32932
|
});
|
|
32595
|
-
const produced = result.messages.slice(
|
|
32933
|
+
const produced = result.messages.slice(result.messages.length - result.appended);
|
|
32596
32934
|
const inputTokens = result.usage.promptTokens ?? 0;
|
|
32597
32935
|
const outputTokens = result.usage.completionTokens ?? 0;
|
|
32598
32936
|
const ceilingAborted = turnAbort.signal.aborted;
|
|
@@ -33465,7 +33803,7 @@ var LineSplitter = class {
|
|
|
33465
33803
|
init_fileService();
|
|
33466
33804
|
|
|
33467
33805
|
// src/session/msb.ts
|
|
33468
|
-
import { spawn as
|
|
33806
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
33469
33807
|
var DEFAULT_LIMITS = {
|
|
33470
33808
|
cpus: 1,
|
|
33471
33809
|
memory: "512M",
|
|
@@ -33512,7 +33850,7 @@ function wslHostPath(p2) {
|
|
|
33512
33850
|
function makeMsbStreamer(bin, wrap) {
|
|
33513
33851
|
return (args, onLine, opts) => {
|
|
33514
33852
|
const { cmd, argv } = wrap ? wrap(args) : { cmd: bin, argv: args };
|
|
33515
|
-
const child =
|
|
33853
|
+
const child = spawn4(cmd, argv, { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
33516
33854
|
const out = new LineSplitter();
|
|
33517
33855
|
const err = new LineSplitter();
|
|
33518
33856
|
let stderrText = "";
|
|
@@ -33563,7 +33901,7 @@ function wslMsb(bin) {
|
|
|
33563
33901
|
function makeMsbRunner(bin, wrap) {
|
|
33564
33902
|
return (args, opts) => new Promise((resolveRun) => {
|
|
33565
33903
|
const { cmd, argv } = wrap ? wrap(args) : { cmd: bin, argv: args };
|
|
33566
|
-
const child =
|
|
33904
|
+
const child = spawn4(cmd, argv, { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
33567
33905
|
let stdout = "";
|
|
33568
33906
|
let stderr = "";
|
|
33569
33907
|
let settled = false;
|
|
@@ -34304,17 +34642,17 @@ function makeGuestRunner(deps) {
|
|
|
34304
34642
|
}
|
|
34305
34643
|
|
|
34306
34644
|
// src/session/sessionsPort.ts
|
|
34307
|
-
function toInfo(h2,
|
|
34645
|
+
function toInfo(h2, backend2) {
|
|
34308
34646
|
return {
|
|
34309
34647
|
id: h2.id,
|
|
34310
34648
|
status: h2.status,
|
|
34311
34649
|
workdir: h2.workdir,
|
|
34312
34650
|
createdAt: h2.createdAt,
|
|
34313
34651
|
...h2.owner ? { owner: h2.owner } : {},
|
|
34314
|
-
isolated:
|
|
34315
|
-
kind:
|
|
34652
|
+
isolated: backend2.isolation !== "none",
|
|
34653
|
+
kind: backend2.kind,
|
|
34316
34654
|
// Feature-detected once, here, rather than by a client calling and failing.
|
|
34317
|
-
canCapture: !!
|
|
34655
|
+
canCapture: !!backend2.commit,
|
|
34318
34656
|
// Read from the HANDLE, so it survives every later list() — and, on the
|
|
34319
34657
|
// microVM backend, a daemon restart, since the sandbox carries it in its
|
|
34320
34658
|
// own labels.
|
|
@@ -34325,9 +34663,9 @@ function toInfo(h2, backend) {
|
|
|
34325
34663
|
};
|
|
34326
34664
|
}
|
|
34327
34665
|
function makeSessionsPort(deps) {
|
|
34328
|
-
const { manager, backend } = deps;
|
|
34666
|
+
const { manager, backend: backend2 } = deps;
|
|
34329
34667
|
return {
|
|
34330
|
-
list: () => manager.list().map((h2) => toInfo(h2,
|
|
34668
|
+
list: () => manager.list().map((h2) => toInfo(h2, backend2)),
|
|
34331
34669
|
async create(spec) {
|
|
34332
34670
|
let source;
|
|
34333
34671
|
let workdir = spec.workdir;
|
|
@@ -34361,7 +34699,7 @@ function makeSessionsPort(deps) {
|
|
|
34361
34699
|
...spec.stateDir ? { stateDir: spec.stateDir } : {},
|
|
34362
34700
|
...env && Object.keys(env).length ? { env } : {}
|
|
34363
34701
|
});
|
|
34364
|
-
return { ...toInfo(h2,
|
|
34702
|
+
return { ...toInfo(h2, backend2), ...source ? { source } : {} };
|
|
34365
34703
|
},
|
|
34366
34704
|
canClone: deps.git?.enabled === true && deps.approveCheckout !== void 0,
|
|
34367
34705
|
/* Straight through to the manager, which is where admission and the
|
|
@@ -34379,9 +34717,9 @@ function makeSessionsPort(deps) {
|
|
|
34379
34717
|
* client feature-detects by asking rather than by calling and failing. A
|
|
34380
34718
|
* process-backed session genuinely cannot capture or stream, and saying so
|
|
34381
34719
|
* up front is better than an error at the moment someone tries. */
|
|
34382
|
-
...
|
|
34720
|
+
...backend2.commit ? {
|
|
34383
34721
|
commit: async (id, name) => {
|
|
34384
|
-
const snap = await
|
|
34722
|
+
const snap = await backend2.commit(id, name);
|
|
34385
34723
|
return {
|
|
34386
34724
|
name: snap.name,
|
|
34387
34725
|
createdAt: snap.createdAt,
|
|
@@ -34389,26 +34727,26 @@ function makeSessionsPort(deps) {
|
|
|
34389
34727
|
};
|
|
34390
34728
|
}
|
|
34391
34729
|
} : {},
|
|
34392
|
-
...
|
|
34393
|
-
snapshots: async () => (await
|
|
34730
|
+
...backend2.snapshots ? {
|
|
34731
|
+
snapshots: async () => (await backend2.snapshots()).map((s2) => ({
|
|
34394
34732
|
name: s2.name,
|
|
34395
34733
|
createdAt: s2.createdAt,
|
|
34396
34734
|
...s2.sessionId ? { sessionId: s2.sessionId } : {},
|
|
34397
34735
|
...s2.sizeBytes !== void 0 ? { sizeBytes: s2.sizeBytes } : {}
|
|
34398
34736
|
}))
|
|
34399
34737
|
} : {},
|
|
34400
|
-
...
|
|
34401
|
-
snapshotUsage: () =>
|
|
34738
|
+
...backend2.snapshotUsage ? {
|
|
34739
|
+
snapshotUsage: () => backend2.snapshotUsage()
|
|
34402
34740
|
} : {},
|
|
34403
|
-
...
|
|
34741
|
+
...backend2.metrics ? {
|
|
34404
34742
|
watchMetrics: (id, onSample) => {
|
|
34405
|
-
const st2 =
|
|
34743
|
+
const st2 = backend2.metrics(id, onSample);
|
|
34406
34744
|
return () => st2.stop();
|
|
34407
34745
|
}
|
|
34408
34746
|
} : {},
|
|
34409
|
-
...
|
|
34747
|
+
...backend2.logs ? {
|
|
34410
34748
|
watchLogs: (id, onLine) => {
|
|
34411
|
-
const st2 =
|
|
34749
|
+
const st2 = backend2.logs(id, onLine);
|
|
34412
34750
|
return () => st2.stop();
|
|
34413
34751
|
}
|
|
34414
34752
|
} : {}
|
|
@@ -34431,16 +34769,16 @@ function portExtras(opts) {
|
|
|
34431
34769
|
}
|
|
34432
34770
|
async function wireSessions(opts) {
|
|
34433
34771
|
if (opts.isolation === "none") {
|
|
34434
|
-
const
|
|
34772
|
+
const backend3 = makeProcessBackend({ roots: opts.roots, ...opts.log ? { log: opts.log } : {} });
|
|
34435
34773
|
opts.log?.("sessions: isolation none (runs execute in the daemon process)");
|
|
34436
34774
|
const manager2 = new SessionManager({
|
|
34437
|
-
backend:
|
|
34775
|
+
backend: backend3,
|
|
34438
34776
|
runner: opts.runner,
|
|
34439
34777
|
...opts.recordRun ? { recordRun: opts.recordRun } : {},
|
|
34440
34778
|
...opts.recordTranscript ? { recordTranscript: opts.recordTranscript } : {},
|
|
34441
34779
|
...opts.log ? { log: opts.log } : {}
|
|
34442
34780
|
});
|
|
34443
|
-
return { manager: manager2, backend:
|
|
34781
|
+
return { manager: manager2, backend: backend3, port: makeSessionsPort({ manager: manager2, backend: backend3, guestEnv, ...portExtras(opts) }) };
|
|
34444
34782
|
}
|
|
34445
34783
|
const viaWsl = process.platform === "win32";
|
|
34446
34784
|
const makeRunner = opts.makeRunner ?? ((bin) => makeMsbRunner(bin, viaWsl ? wslMsb(bin) : void 0));
|
|
@@ -34453,7 +34791,7 @@ async function wireSessions(opts) {
|
|
|
34453
34791
|
On Windows msb runs inside WSL2 \u2014 there is no native build \u2014 so the daemon must run there too.`
|
|
34454
34792
|
);
|
|
34455
34793
|
}
|
|
34456
|
-
const
|
|
34794
|
+
const backend2 = makeMicrovmBackend({
|
|
34457
34795
|
roots: opts.roots,
|
|
34458
34796
|
run,
|
|
34459
34797
|
stream: makeMsbStreamer(opts.msbBin, viaWsl ? wslMsb(opts.msbBin) : void 0),
|
|
@@ -34464,14 +34802,14 @@ async function wireSessions(opts) {
|
|
|
34464
34802
|
...opts.log ? { log: opts.log } : {}
|
|
34465
34803
|
});
|
|
34466
34804
|
const manager = new SessionManager({
|
|
34467
|
-
backend,
|
|
34805
|
+
backend: backend2,
|
|
34468
34806
|
runner: opts.runner,
|
|
34469
34807
|
...opts.recordRun ? { recordRun: opts.recordRun } : {},
|
|
34470
34808
|
...opts.recordTranscript ? { recordTranscript: opts.recordTranscript } : {},
|
|
34471
34809
|
// One runner per session, built on demand: the manager knows which session a
|
|
34472
34810
|
// run belongs to, so nothing has to infer it.
|
|
34473
34811
|
guestRunnerFor: (sessionId, guestOpts) => makeGuestRunner({
|
|
34474
|
-
backend,
|
|
34812
|
+
backend: backend2,
|
|
34475
34813
|
sessionId: () => sessionId,
|
|
34476
34814
|
allowMutations: guestOpts.allowMutations,
|
|
34477
34815
|
...opts.log ? { log: opts.log } : {}
|
|
@@ -34479,19 +34817,19 @@ async function wireSessions(opts) {
|
|
|
34479
34817
|
...opts.log ? { log: opts.log } : {}
|
|
34480
34818
|
});
|
|
34481
34819
|
opts.log?.(`sessions: isolation microvm via ${version} (image ${opts.image})`);
|
|
34482
|
-
await
|
|
34820
|
+
await backend2.adopt?.().catch((e2) => {
|
|
34483
34821
|
opts.log?.(`sessions: adoption failed (${e2 instanceof Error ? e2.message : String(e2)}) \u2014 starting with none adopted`);
|
|
34484
34822
|
});
|
|
34485
|
-
return { manager, backend, port: makeSessionsPort({ manager, backend, guestEnv, ...portExtras(opts) }) };
|
|
34823
|
+
return { manager, backend: backend2, port: makeSessionsPort({ manager, backend: backend2, guestEnv, ...portExtras(opts) }) };
|
|
34486
34824
|
}
|
|
34487
34825
|
|
|
34488
34826
|
// src/runner/crewSessions.ts
|
|
34489
34827
|
function makeCrewSessions(src2, opts) {
|
|
34490
34828
|
if (!src2?.manager.isolated) return void 0;
|
|
34491
|
-
const { manager, backend } = src2;
|
|
34829
|
+
const { manager, backend: backend2 } = src2;
|
|
34492
34830
|
return {
|
|
34493
|
-
list: () =>
|
|
34494
|
-
adopt: () =>
|
|
34831
|
+
list: () => backend2.list(),
|
|
34832
|
+
adopt: () => backend2.adopt?.() ?? Promise.resolve(void 0),
|
|
34495
34833
|
create: async (spec) => {
|
|
34496
34834
|
const env = await opts.guestEnv().catch(() => ({}));
|
|
34497
34835
|
return manager.create({
|
|
@@ -34507,15 +34845,15 @@ function makeCrewSessions(src2, opts) {
|
|
|
34507
34845
|
|
|
34508
34846
|
// src/runner/owners.ts
|
|
34509
34847
|
init_tools_node();
|
|
34510
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
34848
|
+
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
34511
34849
|
import { createHash as createHash2 } from "node:crypto";
|
|
34512
|
-
import { join as
|
|
34850
|
+
import { join as join9, resolve as resolve6 } from "node:path";
|
|
34513
34851
|
init_protocol();
|
|
34514
34852
|
|
|
34515
34853
|
// src/hostsDir.ts
|
|
34516
34854
|
init_tools_node();
|
|
34517
34855
|
init_protocol();
|
|
34518
|
-
import { join as
|
|
34856
|
+
import { join as join8 } from "node:path";
|
|
34519
34857
|
var HOSTS_SEG = "hosts";
|
|
34520
34858
|
var MAX_SYNC_PATH_LEN = 400;
|
|
34521
34859
|
var MAX_SYNC_SEGMENTS = 24;
|
|
@@ -34537,11 +34875,11 @@ function assertSyncPath(path) {
|
|
|
34537
34875
|
}
|
|
34538
34876
|
}
|
|
34539
34877
|
function hostsRoot(stateDir) {
|
|
34540
|
-
return
|
|
34878
|
+
return join8(stateDir, "sidecar", HOSTS_SEG);
|
|
34541
34879
|
}
|
|
34542
34880
|
function hostsDirFor(stateDir, dirName2) {
|
|
34543
34881
|
if (!DIR_NAME_RE.test(dirName2)) throw new Error(`invalid host dir name: ${dirName2}`);
|
|
34544
|
-
return
|
|
34882
|
+
return join8(hostsRoot(stateDir), dirName2);
|
|
34545
34883
|
}
|
|
34546
34884
|
var RAW = /* @__PURE__ */ new WeakMap();
|
|
34547
34885
|
function hostFilesFor(stateDir, dirName2) {
|
|
@@ -34581,7 +34919,7 @@ var WORKSPACE_ID_RE = /^[a-z0-9_-]{1,32}$/;
|
|
|
34581
34919
|
async function workspaceRootsFor(dirName2, hostDir) {
|
|
34582
34920
|
let entries;
|
|
34583
34921
|
try {
|
|
34584
|
-
entries = await readdir(
|
|
34922
|
+
entries = await readdir(join9(hostDir, WORKSPACES_SEG), { withFileTypes: true });
|
|
34585
34923
|
} catch {
|
|
34586
34924
|
return [];
|
|
34587
34925
|
}
|
|
@@ -34589,7 +34927,7 @@ async function workspaceRootsFor(dirName2, hostDir) {
|
|
|
34589
34927
|
for (const entry of entries) {
|
|
34590
34928
|
if (!entry.isDirectory()) continue;
|
|
34591
34929
|
if (!WORKSPACE_ID_RE.test(entry.name)) continue;
|
|
34592
|
-
const wsDir =
|
|
34930
|
+
const wsDir = join9(hostDir, WORKSPACES_SEG, entry.name);
|
|
34593
34931
|
out.push({
|
|
34594
34932
|
owner: `${dirName2}/ws/${entry.name}`,
|
|
34595
34933
|
files: new NodeFileService(wsDir),
|
|
@@ -34602,7 +34940,7 @@ async function workspaceRootsFor(dirName2, hostDir) {
|
|
|
34602
34940
|
async function readCrewOptIn(root, dotDir = stateDirName()) {
|
|
34603
34941
|
let raw;
|
|
34604
34942
|
try {
|
|
34605
|
-
raw = await
|
|
34943
|
+
raw = await readFile2(join9(root, dotDir, CREW_OPT_IN_FILE), "utf8");
|
|
34606
34944
|
} catch {
|
|
34607
34945
|
return null;
|
|
34608
34946
|
}
|
|
@@ -34710,8 +35048,8 @@ async function listOwnerRoots(stateDir, workRoots = [], dotDir = stateDirName())
|
|
|
34710
35048
|
}
|
|
34711
35049
|
|
|
34712
35050
|
// src/crewDeployment.ts
|
|
34713
|
-
import { readFile as
|
|
34714
|
-
import { join as
|
|
35051
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
35052
|
+
import { join as join10 } from "node:path";
|
|
34715
35053
|
function splitOwner(owner) {
|
|
34716
35054
|
if (owner === "") return void 0;
|
|
34717
35055
|
const parts = owner.split("/");
|
|
@@ -34724,7 +35062,7 @@ async function deployedRoot(stateDir, owner, agent) {
|
|
|
34724
35062
|
if (!parts) return void 0;
|
|
34725
35063
|
let text;
|
|
34726
35064
|
try {
|
|
34727
|
-
text = await
|
|
35065
|
+
text = await readFile3(join10(hostsDirFor(stateDir, parts.dirName), DEPLOYMENT_FILE), "utf8");
|
|
34728
35066
|
} catch {
|
|
34729
35067
|
return void 0;
|
|
34730
35068
|
}
|
|
@@ -34737,7 +35075,7 @@ init_tools_node();
|
|
|
34737
35075
|
// src/cert.ts
|
|
34738
35076
|
import { promises as fs9 } from "node:fs";
|
|
34739
35077
|
import { networkInterfaces as networkInterfaces2 } from "node:os";
|
|
34740
|
-
import { join as
|
|
35078
|
+
import { join as join11 } from "node:path";
|
|
34741
35079
|
import selfsigned from "selfsigned";
|
|
34742
35080
|
function lanIPv4s() {
|
|
34743
35081
|
const out = [];
|
|
@@ -34748,9 +35086,9 @@ function lanIPv4s() {
|
|
|
34748
35086
|
}
|
|
34749
35087
|
return [...new Set(out)];
|
|
34750
35088
|
}
|
|
34751
|
-
var certPath = () =>
|
|
34752
|
-
var keyPath = () =>
|
|
34753
|
-
var sansPath = () =>
|
|
35089
|
+
var certPath = () => join11(sidecarSubdir(), "sidecar.crt");
|
|
35090
|
+
var keyPath = () => join11(sidecarSubdir(), "sidecar.key");
|
|
35091
|
+
var sansPath = () => join11(sidecarSubdir(), "sidecar.sans.json");
|
|
34754
35092
|
function selfSignedPaths() {
|
|
34755
35093
|
return { cert: certPath(), key: keyPath() };
|
|
34756
35094
|
}
|
|
@@ -34822,7 +35160,7 @@ function tunnelIds() {
|
|
|
34822
35160
|
}
|
|
34823
35161
|
|
|
34824
35162
|
// src/tunnel/zrok.ts
|
|
34825
|
-
import { spawn as
|
|
35163
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
34826
35164
|
function extractShareUrl(s2) {
|
|
34827
35165
|
const full = s2.match(/https?:\/\/[A-Za-z0-9.-]+(?::\d+)?(?:\/[^\s"'\\]*)?/);
|
|
34828
35166
|
if (full) return full[0];
|
|
@@ -34868,7 +35206,7 @@ function stopChild(child) {
|
|
|
34868
35206
|
}
|
|
34869
35207
|
function reserveName(bin, name) {
|
|
34870
35208
|
return new Promise((res, rej) => {
|
|
34871
|
-
const child =
|
|
35209
|
+
const child = spawn5(bin, ["create", "name", name], { stdio: ["ignore", "ignore", "ignore"] });
|
|
34872
35210
|
let settled = false;
|
|
34873
35211
|
const done = (fn) => {
|
|
34874
35212
|
if (settled) return;
|
|
@@ -34890,7 +35228,7 @@ function reserveName(bin, name) {
|
|
|
34890
35228
|
async function startWith(bin, port, name) {
|
|
34891
35229
|
if (name) await reserveName(bin, name);
|
|
34892
35230
|
return new Promise((resolveHandle, reject) => {
|
|
34893
|
-
const child =
|
|
35231
|
+
const child = spawn5(bin, shareArgs(port, name), { stdio: ["ignore", "pipe", "pipe"] });
|
|
34894
35232
|
let out = "";
|
|
34895
35233
|
let settled = false;
|
|
34896
35234
|
const finish = (fn) => {
|
|
@@ -34939,7 +35277,7 @@ var zrokProvider = {
|
|
|
34939
35277
|
};
|
|
34940
35278
|
|
|
34941
35279
|
// src/tunnel/cloudflared.ts
|
|
34942
|
-
import { spawn as
|
|
35280
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
34943
35281
|
function extractTunnelUrl(s2) {
|
|
34944
35282
|
const m2 = s2.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
|
|
34945
35283
|
return m2 ? m2[0] : null;
|
|
@@ -34993,7 +35331,7 @@ function stopChild2(child) {
|
|
|
34993
35331
|
}
|
|
34994
35332
|
function startWith2(bin, mode, log, onDown) {
|
|
34995
35333
|
return new Promise((resolveHandle, reject) => {
|
|
34996
|
-
const child =
|
|
35334
|
+
const child = spawn6(bin, mode.argv, { stdio: ["ignore", "pipe", "pipe"] });
|
|
34997
35335
|
let out = "";
|
|
34998
35336
|
let settled = false;
|
|
34999
35337
|
let tailBuf = "";
|
|
@@ -35162,7 +35500,7 @@ async function superviseTunnel(opts) {
|
|
|
35162
35500
|
// src/runner/runnerService.ts
|
|
35163
35501
|
init_protocol();
|
|
35164
35502
|
init_tools_node();
|
|
35165
|
-
import { join as
|
|
35503
|
+
import { join as join12 } from "node:path";
|
|
35166
35504
|
|
|
35167
35505
|
// src/runner/location.ts
|
|
35168
35506
|
init_tools_node();
|
|
@@ -41921,7 +42259,7 @@ function nodeGatewayClient(endpoint, stateDir) {
|
|
|
41921
42259
|
}
|
|
41922
42260
|
|
|
41923
42261
|
// src/runner/executor.ts
|
|
41924
|
-
import { mkdir } from "node:fs/promises";
|
|
42262
|
+
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
41925
42263
|
|
|
41926
42264
|
// ../harness/core/recovery.ts
|
|
41927
42265
|
var DEFAULT_RECOVERY = {
|
|
@@ -42135,6 +42473,131 @@ function formatOffloadRef(ref) {
|
|
|
42135
42473
|
${ref.preview}${ref.bytes > ref.preview.length ? "\n\u2026(truncated)" : ""}` : head;
|
|
42136
42474
|
}
|
|
42137
42475
|
|
|
42476
|
+
// ../harness/core/contextPressure.ts
|
|
42477
|
+
var CONTEXT_MARKER = "[context] ";
|
|
42478
|
+
var PRESSURE_BANDS = [0.6, 0.8, 0.9];
|
|
42479
|
+
var PRESSURE_HYSTERESIS = 0.05;
|
|
42480
|
+
var initialPressureGate = { crossed: 0 };
|
|
42481
|
+
function highestBandAtOrBelow(fill, slack = 0) {
|
|
42482
|
+
let best = 0;
|
|
42483
|
+
for (const b2 of PRESSURE_BANDS) if (fill >= b2 - slack) best = b2;
|
|
42484
|
+
return best;
|
|
42485
|
+
}
|
|
42486
|
+
function pressureDecision(fill, gate) {
|
|
42487
|
+
const current2 = highestBandAtOrBelow(fill);
|
|
42488
|
+
if (current2 > gate.crossed) return { band: current2, next: { crossed: current2 } };
|
|
42489
|
+
const held = highestBandAtOrBelow(fill, PRESSURE_HYSTERESIS);
|
|
42490
|
+
return { band: null, next: { crossed: Math.min(gate.crossed, held) } };
|
|
42491
|
+
}
|
|
42492
|
+
function pressureMessage(band, fill) {
|
|
42493
|
+
const pct = Math.round(fill * 100);
|
|
42494
|
+
switch (band) {
|
|
42495
|
+
case 0.6:
|
|
42496
|
+
return `${CONTEXT_MARKER}Context is ${pct}% full. Read files in ranges rather than whole, summarize tool output rather than quoting it back, and stop re-reading things already in this conversation.`;
|
|
42497
|
+
case 0.8:
|
|
42498
|
+
return `${CONTEXT_MARKER}Context is ${pct}% full. Finish the task in progress and report it; start no new exploration, and open nothing large. If the work does not fit, say what is left rather than beginning it.`;
|
|
42499
|
+
case 0.9:
|
|
42500
|
+
return `${CONTEXT_MARKER}Context is ${pct}% full. Stop gathering and answer now with what you already have, naming what you did not get to. Further reading will be lost.`;
|
|
42501
|
+
}
|
|
42502
|
+
}
|
|
42503
|
+
function contextWindowLine(contextWindow) {
|
|
42504
|
+
if (!contextWindow || contextWindow <= 0) return "";
|
|
42505
|
+
return `Your context window is ${contextWindow.toLocaleString("en-US")} tokens. You will be told when it is filling up.`;
|
|
42506
|
+
}
|
|
42507
|
+
function isContextNote(m2) {
|
|
42508
|
+
return m2.role === "assistant" && typeof m2.content === "string" && m2.content.startsWith(CONTEXT_MARKER);
|
|
42509
|
+
}
|
|
42510
|
+
function contextNote(band, fill) {
|
|
42511
|
+
return { role: "assistant", content: pressureMessage(band, fill) };
|
|
42512
|
+
}
|
|
42513
|
+
|
|
42514
|
+
// ../harness/core/compactHistory.ts
|
|
42515
|
+
var CHECKPOINT_MARKER = "[checkpoint] ";
|
|
42516
|
+
var ANCHOR_MARKER = "[anchor] ";
|
|
42517
|
+
var COMPACT_AT_FILL = 0.86;
|
|
42518
|
+
function shouldCompact(fill, messages) {
|
|
42519
|
+
if (fill < COMPACT_AT_FILL) return false;
|
|
42520
|
+
return messages.some((m2) => m2.role !== "system" && m2.role !== "user");
|
|
42521
|
+
}
|
|
42522
|
+
var REARM_BELOW_FILL = 0.6;
|
|
42523
|
+
var REARM_ON_REGROWTH = 0.1;
|
|
42524
|
+
function afterCompaction() {
|
|
42525
|
+
return { armed: false, floor: null };
|
|
42526
|
+
}
|
|
42527
|
+
function messageChars(m2) {
|
|
42528
|
+
let n2 = 0;
|
|
42529
|
+
const content = m2.content;
|
|
42530
|
+
if (typeof content === "string") n2 += content.length;
|
|
42531
|
+
else if (Array.isArray(content)) {
|
|
42532
|
+
for (const part of content) {
|
|
42533
|
+
const text = part.text;
|
|
42534
|
+
if (typeof text === "string") n2 += text.length;
|
|
42535
|
+
}
|
|
42536
|
+
}
|
|
42537
|
+
const calls = m2.tool_calls;
|
|
42538
|
+
if (Array.isArray(calls)) {
|
|
42539
|
+
for (const c2 of calls) n2 += (c2.function?.name?.length ?? 0) + (c2.function?.arguments?.length ?? 0);
|
|
42540
|
+
}
|
|
42541
|
+
return n2;
|
|
42542
|
+
}
|
|
42543
|
+
function estimateFill(messages, contextWindow) {
|
|
42544
|
+
if (!contextWindow || contextWindow <= 0) return 0;
|
|
42545
|
+
let chars = 0;
|
|
42546
|
+
for (const m2 of messages) chars += messageChars(m2);
|
|
42547
|
+
return Math.min(1, chars / 4 / contextWindow);
|
|
42548
|
+
}
|
|
42549
|
+
function compactionDecision(fill, messages, gate) {
|
|
42550
|
+
if (!gate.armed) {
|
|
42551
|
+
const drained = fill < REARM_BELOW_FILL;
|
|
42552
|
+
const regrown = gate.floor !== null && fill >= gate.floor + REARM_ON_REGROWTH;
|
|
42553
|
+
return { compact: false, next: { ...gate, armed: drained || regrown } };
|
|
42554
|
+
}
|
|
42555
|
+
if (!shouldCompact(fill, messages)) return { compact: false, next: gate };
|
|
42556
|
+
return { compact: true, next: afterCompaction() };
|
|
42557
|
+
}
|
|
42558
|
+
async function compactHistory(messages, opts) {
|
|
42559
|
+
const tail3 = messages.length ? messages[messages.length - 1] : void 0;
|
|
42560
|
+
const pending = tail3 && isUnansweredToolCall(tail3) ? tail3 : null;
|
|
42561
|
+
const body2 = pending ? messages.slice(0, -1) : messages;
|
|
42562
|
+
const system = body2.filter((m2) => m2.role === "system");
|
|
42563
|
+
const collapsed = body2.filter((m2) => m2.role !== "system" && m2.role !== "user" && !isContextNote(m2));
|
|
42564
|
+
const kept = [];
|
|
42565
|
+
let answered = null;
|
|
42566
|
+
for (const m2 of body2) {
|
|
42567
|
+
if (isOwnMarker(m2) || isContextNote(m2)) continue;
|
|
42568
|
+
if (m2.role === "assistant" && typeof m2.content === "string") answered = m2;
|
|
42569
|
+
if (m2.role !== "user") continue;
|
|
42570
|
+
if (opts.anchor) {
|
|
42571
|
+
kept.push({ role: "assistant", content: `${ANCHOR_MARKER}${await opts.anchor(m2, answered)}` });
|
|
42572
|
+
}
|
|
42573
|
+
kept.push(await offloadIfOversized(m2, opts));
|
|
42574
|
+
}
|
|
42575
|
+
const summary = await opts.summarize(collapsed);
|
|
42576
|
+
return {
|
|
42577
|
+
messages: [
|
|
42578
|
+
...system,
|
|
42579
|
+
{ role: "assistant", content: `${CHECKPOINT_MARKER}${summary}` },
|
|
42580
|
+
...kept,
|
|
42581
|
+
...pending ? [pending] : []
|
|
42582
|
+
],
|
|
42583
|
+
checkpoint: summary
|
|
42584
|
+
};
|
|
42585
|
+
}
|
|
42586
|
+
async function offloadIfOversized(m2, opts) {
|
|
42587
|
+
const limit2 = opts.offloadOverBytes;
|
|
42588
|
+
if (!opts.offload || limit2 === void 0) return m2;
|
|
42589
|
+
if (typeof m2.content !== "string" || m2.content.length <= limit2) return m2;
|
|
42590
|
+
return { ...m2, content: await opts.offload(m2.content) };
|
|
42591
|
+
}
|
|
42592
|
+
var isCompactionMarker = isOwnMarker;
|
|
42593
|
+
function isOwnMarker(m2) {
|
|
42594
|
+
if (m2.role !== "assistant" || typeof m2.content !== "string") return false;
|
|
42595
|
+
return m2.content.startsWith(ANCHOR_MARKER) || m2.content.startsWith(CHECKPOINT_MARKER);
|
|
42596
|
+
}
|
|
42597
|
+
function isUnansweredToolCall(m2) {
|
|
42598
|
+
return m2.role === "assistant" && Array.isArray(m2.tool_calls) && m2.tool_calls.length > 0;
|
|
42599
|
+
}
|
|
42600
|
+
|
|
42138
42601
|
// ../harness/prompts/toolsMarker.ts
|
|
42139
42602
|
var TOOLS_MARKER = "\uE000PROMPD_TOOLS\uE000";
|
|
42140
42603
|
function formatToolLines(tools) {
|
|
@@ -42255,8 +42718,11 @@ async function runAgent(args) {
|
|
|
42255
42718
|
const ctxWindow = compression?.contextWindow;
|
|
42256
42719
|
const compressionStrategy = compression?.strategy ?? "eager";
|
|
42257
42720
|
let fill = 0;
|
|
42721
|
+
let pressureGate = initialPressureGate;
|
|
42722
|
+
let compactGate = { armed: true, floor: null };
|
|
42258
42723
|
const deferredOffloads = [];
|
|
42259
|
-
const maxTurns = policy.maxTurns
|
|
42724
|
+
const maxTurns = policy.maxTurns !== void 0 ? policy.maxTurns : args.maxTurns !== void 0 ? args.maxTurns : 40;
|
|
42725
|
+
const unlimitedTurns = maxTurns === 0;
|
|
42260
42726
|
const signal = args.signal ?? new AbortController().signal;
|
|
42261
42727
|
const emit = (e2) => args.onEvent?.(e2);
|
|
42262
42728
|
const gstate = initialState();
|
|
@@ -42272,15 +42738,24 @@ async function runAgent(args) {
|
|
|
42272
42738
|
turnAc?.abort();
|
|
42273
42739
|
});
|
|
42274
42740
|
const messages = [...args.messages];
|
|
42741
|
+
const incoming = new Set(args.messages);
|
|
42742
|
+
const appendedFromTail = () => {
|
|
42743
|
+
let n2 = 0;
|
|
42744
|
+
for (let i2 = messages.length - 1; i2 >= 0; i2--) {
|
|
42745
|
+
const m2 = messages[i2];
|
|
42746
|
+
if (incoming.has(m2) || m2.role === "system" || isCompactionMarker(m2)) break;
|
|
42747
|
+
n2++;
|
|
42748
|
+
}
|
|
42749
|
+
return n2;
|
|
42750
|
+
};
|
|
42275
42751
|
const deferred = args.deferredTools ?? [];
|
|
42276
42752
|
const loader = deferred.length ? createToolLoader({ deferred, loaded: args.loadedTools, onChange: (names) => emit({ type: "tools-loaded", names }) }) : null;
|
|
42277
42753
|
const eager = loader ? [...tools, loader.searchTool] : tools;
|
|
42278
42754
|
const capped = (list) => list.length > MAX_PROVIDER_TOOLS ? list.slice(0, MAX_PROVIDER_TOOLS) : list;
|
|
42279
42755
|
const activeTools = () => capped(loader ? [...eager, ...loader.loaded()] : eager);
|
|
42280
42756
|
let sent = activeTools();
|
|
42281
|
-
const
|
|
42282
|
-
|
|
42283
|
-
${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
42757
|
+
const systemParts = [system, loader ? serverIndex(deferred) : "", contextWindowLine(ctxWindow)].filter(Boolean);
|
|
42758
|
+
const indexedSystem = systemParts.length ? systemParts.join("\n\n") : system;
|
|
42284
42759
|
const filledSystem = indexedSystem ? fillToolsMarker(indexedSystem, sent) : indexedSystem;
|
|
42285
42760
|
if (filledSystem && !messages.some((m2) => m2.role === "system")) {
|
|
42286
42761
|
messages.unshift({ role: "system", content: filledSystem });
|
|
@@ -42300,7 +42775,7 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42300
42775
|
let stopReason = "stop";
|
|
42301
42776
|
let lastToolSig = null;
|
|
42302
42777
|
let lastStreamErr = "";
|
|
42303
|
-
for (let turn = 1; turn <= maxTurns; turn++) {
|
|
42778
|
+
for (let turn = 1; unlimitedTurns || turn <= maxTurns; turn++) {
|
|
42304
42779
|
if (signal.aborted) {
|
|
42305
42780
|
stopReason = "aborted";
|
|
42306
42781
|
break;
|
|
@@ -42313,13 +42788,53 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42313
42788
|
emit({ type: "injected", content: typeof m2.content === "string" ? m2.content : "" });
|
|
42314
42789
|
}
|
|
42315
42790
|
}
|
|
42316
|
-
|
|
42317
|
-
const
|
|
42318
|
-
|
|
42319
|
-
|
|
42320
|
-
|
|
42321
|
-
|
|
42322
|
-
|
|
42791
|
+
const gateFill = fill || estimateFill(messages, ctxWindow);
|
|
42792
|
+
const gate = compactionDecision(gateFill, messages, compactGate);
|
|
42793
|
+
compactGate = gate.next;
|
|
42794
|
+
if (args.compaction && gate.compact) {
|
|
42795
|
+
const before = messages.length;
|
|
42796
|
+
try {
|
|
42797
|
+
const out = await compactHistory(messages, {
|
|
42798
|
+
summarize: args.compaction.summarize,
|
|
42799
|
+
anchor: args.compaction.anchor,
|
|
42800
|
+
offloadOverBytes: compression && compression.mode !== "off" ? effectiveThreshold(compression.mode, gateFill) : void 0,
|
|
42801
|
+
offload: toolCtx.recall ? async (content) => formatOffloadRef(await toolCtx.recall.record("user", content)) : void 0
|
|
42802
|
+
});
|
|
42803
|
+
messages.splice(0, messages.length, ...out.messages);
|
|
42804
|
+
deferredOffloads.length = 0;
|
|
42805
|
+
try {
|
|
42806
|
+
await args.compaction.onCheckpoint?.(out.checkpoint);
|
|
42807
|
+
} catch (e2) {
|
|
42808
|
+
alog("checkpoint persist failed", e2 instanceof Error ? e2.message : String(e2));
|
|
42809
|
+
}
|
|
42810
|
+
emit({ type: "checkpoint", collapsed: before - messages.length, checkpoint: out.checkpoint });
|
|
42811
|
+
} catch (e2) {
|
|
42812
|
+
compactGate = { ...compactGate, armed: true };
|
|
42813
|
+
alog("compaction failed", e2 instanceof Error ? e2.message : String(e2));
|
|
42814
|
+
}
|
|
42815
|
+
}
|
|
42816
|
+
const pressureFill = fill || estimateFill(messages, ctxWindow);
|
|
42817
|
+
const pressure = pressureDecision(pressureFill, pressureGate);
|
|
42818
|
+
pressureGate = pressure.next;
|
|
42819
|
+
if (pressure.band !== null && ctxWindow) {
|
|
42820
|
+
messages.push(contextNote(pressure.band, pressureFill));
|
|
42821
|
+
emit({
|
|
42822
|
+
type: "context-pressure",
|
|
42823
|
+
band: pressure.band,
|
|
42824
|
+
fill: pressureFill,
|
|
42825
|
+
contextWindow: ctxWindow,
|
|
42826
|
+
promptTokens: Math.round(pressureFill * ctxWindow)
|
|
42827
|
+
});
|
|
42828
|
+
}
|
|
42829
|
+
if (!unlimitedTurns) {
|
|
42830
|
+
gstate.turn = turn - 1;
|
|
42831
|
+
const turnGate = evaluateTurnStart(policy, gstate);
|
|
42832
|
+
if (!turnGate.ok) {
|
|
42833
|
+
emit({ type: "policy-violation", kind: turnGate.violation.kind, message: turnGate.violation.message, action: turnGate.action, tool: turnGate.violation.tool });
|
|
42834
|
+
if (turnGate.action === "stop") {
|
|
42835
|
+
stopReason = "policy";
|
|
42836
|
+
break;
|
|
42837
|
+
}
|
|
42323
42838
|
}
|
|
42324
42839
|
}
|
|
42325
42840
|
if (loader) {
|
|
@@ -42407,6 +42922,7 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42407
42922
|
usage.completionTokens += streamed.usage.completionTokens;
|
|
42408
42923
|
if (ctxWindow && ctxWindow > 0 && streamed.usage.promptTokens) {
|
|
42409
42924
|
fill = Math.min(1, streamed.usage.promptTokens / ctxWindow);
|
|
42925
|
+
if (!compactGate.armed && compactGate.floor === null) compactGate = { armed: false, floor: fill };
|
|
42410
42926
|
}
|
|
42411
42927
|
emit({ type: "usage", usage: streamed.usage });
|
|
42412
42928
|
const assistantMsg = streamed.toolCalls.length ? {
|
|
@@ -42455,13 +42971,13 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42455
42971
|
}
|
|
42456
42972
|
loader?.touch(tool.name);
|
|
42457
42973
|
const address = toolAddress(tool);
|
|
42458
|
-
const
|
|
42459
|
-
if (!
|
|
42460
|
-
const msg = `Blocked by policy "${policy.name}": ${
|
|
42974
|
+
const gate2 = evaluateToolCall(policy, gstate, { name: call.name, exists: true, address });
|
|
42975
|
+
if (!gate2.ok) {
|
|
42976
|
+
const msg = `Blocked by policy "${policy.name}": ${gate2.violation.message}`;
|
|
42461
42977
|
messages.push({ role: "tool", tool_call_id: call.id, content: `Error: ${msg}` });
|
|
42462
|
-
emit({ type: "policy-violation", kind:
|
|
42978
|
+
emit({ type: "policy-violation", kind: gate2.violation.kind, message: gate2.violation.message, action: gate2.action, tool: call.name });
|
|
42463
42979
|
emit({ type: "tool-result", id: call.id, name: call.name, result: msg, isError: true });
|
|
42464
|
-
if (
|
|
42980
|
+
if (gate2.action === "stop") {
|
|
42465
42981
|
policyStop = true;
|
|
42466
42982
|
break;
|
|
42467
42983
|
}
|
|
@@ -42536,7 +43052,7 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42536
43052
|
stopReason = "policy";
|
|
42537
43053
|
break;
|
|
42538
43054
|
}
|
|
42539
|
-
if (turn === maxTurns) stopReason = "max_turns";
|
|
43055
|
+
if (!unlimitedTurns && turn === maxTurns) stopReason = "max_turns";
|
|
42540
43056
|
}
|
|
42541
43057
|
reconcileDanglingToolCalls(messages);
|
|
42542
43058
|
for (const v2 of evaluateCompletion(policy, gstate)) {
|
|
@@ -42544,7 +43060,7 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42544
43060
|
}
|
|
42545
43061
|
alog("done", stopReason, "tokens", usage);
|
|
42546
43062
|
emit({ type: "done", reason: stopReason });
|
|
42547
|
-
return { messages, text, usage, stopReason };
|
|
43063
|
+
return { messages, text, usage, stopReason, appended: appendedFromTail() };
|
|
42548
43064
|
}
|
|
42549
43065
|
async function compactIncomingOffloads(messages, toolByName, compression, recall, emit) {
|
|
42550
43066
|
const nameForCallId = /* @__PURE__ */ new Map();
|
|
@@ -42630,8 +43146,29 @@ var JsonlSessionStore = class _JsonlSessionStore {
|
|
|
42630
43146
|
if (!f2) throw new Error("No file workspace is connected \u2014 sessions are unavailable.");
|
|
42631
43147
|
return f2;
|
|
42632
43148
|
}
|
|
43149
|
+
/* Sessions that have been MOVED out from under this store's root, id -> the
|
|
43150
|
+
* root they now live in.
|
|
43151
|
+
*
|
|
43152
|
+
* A session can move mid-run: start_project offers to bring the conversation
|
|
43153
|
+
* into the project it just created, and the run that asked is still writing.
|
|
43154
|
+
* Without this, the next save() would resolve the OLD path and recreate the
|
|
43155
|
+
* file there from its cursor -- the same conversation in two places, neither
|
|
43156
|
+
* complete. The cursor itself survives the move untouched: it is keyed by id
|
|
43157
|
+
* and holds the text, not the path.
|
|
43158
|
+
*
|
|
43159
|
+
* In memory only, and deliberately: it describes where a session went during
|
|
43160
|
+
* THIS page's life. A reload lists both roots afresh and needs no memory of
|
|
43161
|
+
* the journey. */
|
|
43162
|
+
movedRoots = /* @__PURE__ */ new Map();
|
|
43163
|
+
/** Resolve this id somewhere other than the store's own root, from now on. */
|
|
43164
|
+
setRootFor(id, root) {
|
|
43165
|
+
this.movedRoots.set(id, root);
|
|
43166
|
+
}
|
|
43167
|
+
rootFor(id) {
|
|
43168
|
+
return this.movedRoots.get(id) ?? this.root();
|
|
43169
|
+
}
|
|
42633
43170
|
path(id) {
|
|
42634
|
-
return `${this.
|
|
43171
|
+
return `${this.rootFor(id)}/${id}/SESSION.jsonl`;
|
|
42635
43172
|
}
|
|
42636
43173
|
async create(title, surface) {
|
|
42637
43174
|
const createdAt = now();
|
|
@@ -42800,10 +43337,108 @@ var JsonlSessionStore = class _JsonlSessionStore {
|
|
|
42800
43337
|
}
|
|
42801
43338
|
async delete(id) {
|
|
42802
43339
|
this.cursors.delete(id);
|
|
42803
|
-
await (await this.files()).remove(`${this.
|
|
43340
|
+
await (await this.files()).remove(`${this.rootFor(id)}/${id}`).catch(() => {
|
|
43341
|
+
});
|
|
43342
|
+
}
|
|
43343
|
+
};
|
|
43344
|
+
|
|
43345
|
+
// ../harness/sessions/checkpointStore.ts
|
|
43346
|
+
var CHECKPOINT_FILE = "CHECKPOINT.md";
|
|
43347
|
+
function makeCheckpointStore(getFiles, root) {
|
|
43348
|
+
return {
|
|
43349
|
+
async append(sessionId, body2) {
|
|
43350
|
+
const files = await getFiles();
|
|
43351
|
+
if (!files) return;
|
|
43352
|
+
const path = `${root}/${sessionId}/${CHECKPOINT_FILE}`;
|
|
43353
|
+
const entry = `## ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
43354
|
+
|
|
43355
|
+
${body2}
|
|
43356
|
+
`;
|
|
43357
|
+
let existing = null;
|
|
43358
|
+
try {
|
|
43359
|
+
existing = await files.read(path);
|
|
43360
|
+
} catch {
|
|
43361
|
+
existing = null;
|
|
43362
|
+
}
|
|
43363
|
+
if (existing === null) {
|
|
43364
|
+
await files.create(path, `# Checkpoints
|
|
43365
|
+
|
|
43366
|
+
${entry}`);
|
|
43367
|
+
return;
|
|
43368
|
+
}
|
|
43369
|
+
await files.write(path, `${existing}
|
|
43370
|
+
${entry}`);
|
|
43371
|
+
}
|
|
43372
|
+
};
|
|
43373
|
+
}
|
|
43374
|
+
|
|
43375
|
+
// ../harness/core/summarize.ts
|
|
43376
|
+
var TRUNCATION_NOTE = " \u2026[truncated]";
|
|
43377
|
+
var MIN_KEEP = 200;
|
|
43378
|
+
var SUMMARY_INPUT_CHARS = 6e4;
|
|
43379
|
+
var SYSTEM = [
|
|
43380
|
+
"You are compacting an agent conversation that is about to lose its middle.",
|
|
43381
|
+
"Write a dense record of what was DECIDED and what the current state is:",
|
|
43382
|
+
"conclusions reached, constraints the user set, files and artifacts touched,",
|
|
43383
|
+
"and anything still open. Omit narration, pleasantries and tool mechanics.",
|
|
43384
|
+
"Write plain prose or short bullets. No preamble."
|
|
43385
|
+
].join(" ");
|
|
43386
|
+
function makeSummarizer(client, model, opts = {}) {
|
|
43387
|
+
const maxChars = opts.maxChars ?? SUMMARY_INPUT_CHARS;
|
|
43388
|
+
return async (collapsed) => {
|
|
43389
|
+
const rendered = renderTail(collapsed, maxChars);
|
|
43390
|
+
const res = await client.chat.completions.create({
|
|
43391
|
+
model,
|
|
43392
|
+
messages: [
|
|
43393
|
+
{ role: "system", content: SYSTEM },
|
|
43394
|
+
{ role: "user", content: rendered }
|
|
43395
|
+
],
|
|
43396
|
+
stream: false
|
|
42804
43397
|
});
|
|
43398
|
+
return res.choices[0]?.message?.content ?? "";
|
|
43399
|
+
};
|
|
43400
|
+
}
|
|
43401
|
+
function renderTail(messages, maxChars) {
|
|
43402
|
+
const lines = [];
|
|
43403
|
+
let used = 0;
|
|
43404
|
+
for (let i2 = messages.length - 1; i2 >= 0; i2--) {
|
|
43405
|
+
const m2 = messages[i2];
|
|
43406
|
+
const text = typeof m2.content === "string" ? m2.content : JSON.stringify(m2.content ?? "");
|
|
43407
|
+
const line = `${m2.role}: ${text}`;
|
|
43408
|
+
const room = maxChars - used;
|
|
43409
|
+
if (line.length > room) {
|
|
43410
|
+
if (room > MIN_KEEP) lines.unshift(line.slice(0, room - TRUNCATION_NOTE.length) + TRUNCATION_NOTE);
|
|
43411
|
+
break;
|
|
43412
|
+
}
|
|
43413
|
+
lines.unshift(line);
|
|
43414
|
+
used += line.length;
|
|
42805
43415
|
}
|
|
43416
|
+
return lines.join("\n\n");
|
|
43417
|
+
}
|
|
43418
|
+
|
|
43419
|
+
// ../harness/providers/contextWindow.ts
|
|
43420
|
+
var KNOWN_WINDOWS = {
|
|
43421
|
+
"claude-sonnet-4-5": 2e5,
|
|
43422
|
+
"claude-haiku-4-5": 2e5,
|
|
43423
|
+
"claude-opus-4-1": 2e5,
|
|
43424
|
+
"gpt-4o-mini": 128e3,
|
|
43425
|
+
"gpt-4.1-mini": 1e6,
|
|
43426
|
+
"gpt-5": 4e5,
|
|
43427
|
+
"gpt-5-mini": 4e5,
|
|
43428
|
+
"gemini-2-5-pro": 1e6,
|
|
43429
|
+
"gemini-2-5-flash": 1e6
|
|
42806
43430
|
};
|
|
43431
|
+
var CONSERVATIVE_FALLBACK_WINDOW = 32e3;
|
|
43432
|
+
function stripDateSuffix(id) {
|
|
43433
|
+
return id.replace(/-\d{4}-\d{2}-\d{2}$/, "").replace(/-\d{8}$/, "");
|
|
43434
|
+
}
|
|
43435
|
+
function resolveContextWindow(modelId) {
|
|
43436
|
+
const id = modelId.trim().toLowerCase();
|
|
43437
|
+
if (id in KNOWN_WINDOWS) return { window: KNOWN_WINDOWS[id], source: "known" };
|
|
43438
|
+
const stripped = stripDateSuffix(id);
|
|
43439
|
+
if (stripped in KNOWN_WINDOWS) return { window: KNOWN_WINDOWS[stripped], source: "known" };
|
|
43440
|
+
return { window: CONSERVATIVE_FALLBACK_WINDOW, source: "fallback" };
|
|
43441
|
+
}
|
|
42807
43442
|
|
|
42808
43443
|
// src/runner/runLog.ts
|
|
42809
43444
|
function runLogPath(crewRoot2, agent) {
|
|
@@ -42872,30 +43507,43 @@ function pricedForUsd(rates) {
|
|
|
42872
43507
|
if (!rates) return false;
|
|
42873
43508
|
return rates.inPrice > 0 || rates.outPrice > 0;
|
|
42874
43509
|
}
|
|
42875
|
-
async function
|
|
42876
|
-
const table = /* @__PURE__ */ new Map();
|
|
43510
|
+
async function fetchAvailableModels(endpoint, getToken, fetchImpl) {
|
|
42877
43511
|
let token = null;
|
|
42878
43512
|
try {
|
|
42879
43513
|
token = await getToken();
|
|
42880
43514
|
} catch {
|
|
42881
43515
|
token = null;
|
|
42882
43516
|
}
|
|
42883
|
-
if (!token) return
|
|
43517
|
+
if (!token) return [];
|
|
42884
43518
|
try {
|
|
42885
43519
|
const res = await fetchImpl(`${endpoint.replace(/\/$/, "")}/llm-providers/available`, {
|
|
42886
43520
|
headers: { Authorization: `Bearer ${token}` }
|
|
42887
43521
|
});
|
|
42888
|
-
if (!res.ok) return
|
|
43522
|
+
if (!res.ok) return [];
|
|
42889
43523
|
const json = await res.json();
|
|
42890
|
-
|
|
42891
|
-
|
|
42892
|
-
|
|
42893
|
-
table.set(m2.model, { inPrice: m2.inputPrice, outPrice: m2.outputPrice });
|
|
42894
|
-
}
|
|
42895
|
-
}
|
|
43524
|
+
const out = [];
|
|
43525
|
+
for (const p2 of json.providers ?? []) out.push(...p2.models ?? []);
|
|
43526
|
+
return out;
|
|
42896
43527
|
} catch {
|
|
43528
|
+
return [];
|
|
43529
|
+
}
|
|
43530
|
+
}
|
|
43531
|
+
async function hydrateCatalog(endpoint, getToken, fetchImpl = fetch) {
|
|
43532
|
+
const prices = /* @__PURE__ */ new Map();
|
|
43533
|
+
const windows = /* @__PURE__ */ new Map();
|
|
43534
|
+
const models = await fetchAvailableModels(endpoint, getToken, fetchImpl);
|
|
43535
|
+
for (const m2 of models) {
|
|
43536
|
+
if (m2.inputPrice != null && m2.outputPrice != null) {
|
|
43537
|
+
prices.set(m2.model, { inPrice: m2.inputPrice, outPrice: m2.outputPrice });
|
|
43538
|
+
}
|
|
43539
|
+
if (typeof m2.contextWindow === "number" && m2.contextWindow > 0) {
|
|
43540
|
+
windows.set(m2.model, m2.contextWindow);
|
|
43541
|
+
}
|
|
42897
43542
|
}
|
|
42898
|
-
return
|
|
43543
|
+
return { prices, windows };
|
|
43544
|
+
}
|
|
43545
|
+
async function hydratePrices(endpoint, getToken, fetchImpl = fetch) {
|
|
43546
|
+
return (await hydrateCatalog(endpoint, getToken, fetchImpl)).prices;
|
|
42899
43547
|
}
|
|
42900
43548
|
|
|
42901
43549
|
// src/runner/permissionMessage.ts
|
|
@@ -42932,7 +43580,7 @@ async function runCrewAgentNode(def, input, ctx, deps) {
|
|
|
42932
43580
|
const signal = ctx.signal ?? new AbortController().signal;
|
|
42933
43581
|
const resolveDef = crewResolveDef(ctx.stateFiles, ctx.stateRoot);
|
|
42934
43582
|
const kernelTools = crewToolSet(def, resolveDef)();
|
|
42935
|
-
await
|
|
43583
|
+
await mkdir2(ctx.memoryDir, { recursive: true });
|
|
42936
43584
|
const extraTools = deps.nodeTools([...ctx.workRoots, ctx.memoryDir]);
|
|
42937
43585
|
const tools = [...kernelTools, ...extraTools];
|
|
42938
43586
|
const conditionalOffers = [];
|
|
@@ -43034,6 +43682,21 @@ async function runCrewAgentNode(def, input, ctx, deps) {
|
|
|
43034
43682
|
// different answer. Supplied only when no prompt is wired; with one, a real
|
|
43035
43683
|
// person really did decide, and the default wording is correct.
|
|
43036
43684
|
...promptImpl ? {} : { describePermissionDenial: unattendedDenialMessage },
|
|
43685
|
+
/* `mode: 'off'` with a real `contextWindow` reads oddly but is exactly what
|
|
43686
|
+
* is wanted here: rung-1 offload stays off (this executor supplies no
|
|
43687
|
+
* `toolCtx.recall`, so there is nowhere to offload TO), while the loop still
|
|
43688
|
+
* computes `fill` — which is the only thing that lets it notice a window
|
|
43689
|
+
* about to overflow. Without a window, `fill` is pinned at 0 and compaction
|
|
43690
|
+
* below is dead code however well it is wired. */
|
|
43691
|
+
compression: { mode: "off", contextWindow: resolveContextWindow(modelId).window },
|
|
43692
|
+
/* Why the daemon in particular: these runs are unattended and long, so
|
|
43693
|
+
* nobody is watching to notice the wall and restart. Before this, hitting
|
|
43694
|
+
* it produced a provider 400 — fatal in recovery.ts, never retried — and
|
|
43695
|
+
* the run simply died mid-task. */
|
|
43696
|
+
compaction: {
|
|
43697
|
+
summarize: makeSummarizer(ctx.client, modelId),
|
|
43698
|
+
onCheckpoint: (body2) => makeCheckpointStore(() => ctx.stateFiles, sessionsRoot2).append(session.id, body2)
|
|
43699
|
+
},
|
|
43037
43700
|
signal: runSignal,
|
|
43038
43701
|
onEvent
|
|
43039
43702
|
});
|
|
@@ -43681,7 +44344,7 @@ var NodeRunnerService = class {
|
|
|
43681
44344
|
workFiles,
|
|
43682
44345
|
workRoots: [workDir],
|
|
43683
44346
|
client: this.client,
|
|
43684
|
-
memoryDir:
|
|
44347
|
+
memoryDir: join12(stateOwnerDir, stateRoot, agent, "memory"),
|
|
43685
44348
|
onEvent: trackedOnEvent,
|
|
43686
44349
|
sessionId,
|
|
43687
44350
|
signal: controller.signal,
|
|
@@ -43718,7 +44381,7 @@ var NodeRunnerService = class {
|
|
|
43718
44381
|
// src/runner/core.ts
|
|
43719
44382
|
import { watch } from "node:fs";
|
|
43720
44383
|
import { readdir as readdir2, stat } from "node:fs/promises";
|
|
43721
|
-
import { join as
|
|
44384
|
+
import { join as join14, relative as relative6, sep as sep4 } from "node:path";
|
|
43722
44385
|
|
|
43723
44386
|
// src/runner/triggerConfig.ts
|
|
43724
44387
|
function inputFor(trigger, fallback) {
|
|
@@ -44671,8 +45334,8 @@ async function resumeOpenRooms(opts) {
|
|
|
44671
45334
|
}
|
|
44672
45335
|
|
|
44673
45336
|
// src/runner/roomDeps.ts
|
|
44674
|
-
import { mkdir as
|
|
44675
|
-
import { join as
|
|
45337
|
+
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
45338
|
+
import { join as join13 } from "node:path";
|
|
44676
45339
|
|
|
44677
45340
|
// src/runner/roomAgentLookup.ts
|
|
44678
45341
|
init_tools_node();
|
|
@@ -44772,11 +45435,11 @@ async function makeDaemonRoomDeps(ctx, room, deps = {}) {
|
|
|
44772
45435
|
`cannot locate the memory folder for crew agent "${def.name}": this daemon has no state root for owner "${ctx.owner}"`
|
|
44773
45436
|
);
|
|
44774
45437
|
}
|
|
44775
|
-
return
|
|
45438
|
+
return join13(stateDir, agentMemoryRoot(ctx.stateRoot, def.name));
|
|
44776
45439
|
}
|
|
44777
45440
|
async function buildRuntime(def, p2, state, findings) {
|
|
44778
45441
|
const memoryDir = memoryDirFor(def);
|
|
44779
|
-
await
|
|
45442
|
+
await mkdir3(memoryDir, { recursive: true });
|
|
44780
45443
|
const toolCtx = {
|
|
44781
45444
|
signal: new AbortController().signal,
|
|
44782
45445
|
memory: makeAgentMemoryProvider(() => ctx.stateFiles, agentMemoryRoot(ctx.stateRoot, def.name)),
|
|
@@ -44826,7 +45489,19 @@ async function makeDaemonRoomDeps(ctx, room, deps = {}) {
|
|
|
44826
45489
|
// person really did decide and the default wording is correct.
|
|
44827
45490
|
...ctx.prompt ? {} : { describePermissionDenial: unattendedDenialMessage },
|
|
44828
45491
|
...ctx.now ? { now: ctx.now } : {},
|
|
44829
|
-
...ceilingUsd !== void 0 ? { turnCeilingUsd: ceilingUsd } : {}
|
|
45492
|
+
...ceilingUsd !== void 0 ? { turnCeilingUsd: ceilingUsd } : {},
|
|
45493
|
+
/* A room is the likeliest place in the product to meet the context
|
|
45494
|
+
* wall: every participant's turn is seeded with the WHOLE transcript,
|
|
45495
|
+
* tool calls included, so cost grows with participants x turns. Before
|
|
45496
|
+
* this, a room that filled the window died on a provider 400 -- fatal
|
|
45497
|
+
* in recovery.ts -- and took the turn with it.
|
|
45498
|
+
*
|
|
45499
|
+
* `mode: 'off'` with a real window is the same trade `executor.ts`
|
|
45500
|
+
* makes: rung-1 offload stays off (this toolCtx supplies no `recall`,
|
|
45501
|
+
* so there is nowhere to offload to) while the loop still computes
|
|
45502
|
+
* `fill`, which is the only thing that lets compaction fire. */
|
|
45503
|
+
compression: { mode: "off", contextWindow: resolveContextWindow(modelId).window },
|
|
45504
|
+
compaction: { summarize: makeSummarizer(ctx.client, modelId) }
|
|
44830
45505
|
}
|
|
44831
45506
|
});
|
|
44832
45507
|
}
|
|
@@ -45072,9 +45747,9 @@ function startDaemonCore(opts) {
|
|
|
45072
45747
|
for (const entry of entries) {
|
|
45073
45748
|
if (entry.isDirectory()) {
|
|
45074
45749
|
if (isExcludedSegment(entry.name, dotDir)) continue;
|
|
45075
|
-
await walkForChanges(root,
|
|
45750
|
+
await walkForChanges(root, join14(dir, entry.name), since);
|
|
45076
45751
|
} else {
|
|
45077
|
-
const abs =
|
|
45752
|
+
const abs = join14(dir, entry.name);
|
|
45078
45753
|
let mtimeMs;
|
|
45079
45754
|
try {
|
|
45080
45755
|
mtimeMs = (await stat(abs)).mtimeMs;
|
|
@@ -45386,9 +46061,9 @@ function makeRunHub(opts = {}) {
|
|
|
45386
46061
|
import { promises as fs11 } from "node:fs";
|
|
45387
46062
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
45388
46063
|
import { hostname } from "node:os";
|
|
45389
|
-
import { join as
|
|
46064
|
+
import { join as join15 } from "node:path";
|
|
45390
46065
|
function idPath(dir = sidecarSubdir()) {
|
|
45391
|
-
return
|
|
46066
|
+
return join15(dir, "daemon.id");
|
|
45392
46067
|
}
|
|
45393
46068
|
async function loadOrCreateSidecarId(dir = sidecarSubdir()) {
|
|
45394
46069
|
const path = idPath(dir);
|
|
@@ -45417,7 +46092,7 @@ function sidecarName(opts) {
|
|
|
45417
46092
|
// src/hosts.ts
|
|
45418
46093
|
import { promises as fs12 } from "node:fs";
|
|
45419
46094
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
45420
|
-
import { join as
|
|
46095
|
+
import { join as join16, dirname as dirname6 } from "node:path";
|
|
45421
46096
|
var MAX_DIR = 32;
|
|
45422
46097
|
var MAX_FIELD_LEN = 200;
|
|
45423
46098
|
var MAX_HOSTS = 200;
|
|
@@ -45451,7 +46126,7 @@ function sanitizeDirName(raw) {
|
|
|
45451
46126
|
return RESERVED_WINDOWS_NAMES.has(safe) ? `${safe}-host` : safe;
|
|
45452
46127
|
}
|
|
45453
46128
|
function hostsPath(stateDir) {
|
|
45454
|
-
return
|
|
46129
|
+
return join16(stateDir, "sidecar", "hosts.json");
|
|
45455
46130
|
}
|
|
45456
46131
|
function emptyManifest2() {
|
|
45457
46132
|
return /* @__PURE__ */ Object.create(null);
|
|
@@ -45491,10 +46166,10 @@ async function read(dir, fallbackLastSeen) {
|
|
|
45491
46166
|
return manifest;
|
|
45492
46167
|
}
|
|
45493
46168
|
async function write(stateDir, manifest) {
|
|
45494
|
-
const dir =
|
|
46169
|
+
const dir = dirname6(hostsPath(stateDir));
|
|
45495
46170
|
await fs12.mkdir(dir, { recursive: true });
|
|
45496
46171
|
const target = hostsPath(stateDir);
|
|
45497
|
-
const tmp =
|
|
46172
|
+
const tmp = join16(dir, `.hosts.json.tmp-${randomBytes3(8).toString("hex")}`);
|
|
45498
46173
|
try {
|
|
45499
46174
|
await fs12.writeFile(tmp, JSON.stringify(manifest, null, 2));
|
|
45500
46175
|
await fs12.rename(tmp, target);
|
|
@@ -45601,7 +46276,7 @@ function makeHostsManifest(dir) {
|
|
|
45601
46276
|
|
|
45602
46277
|
// src/hostsAdmin.ts
|
|
45603
46278
|
import { readdir as readdir3 } from "node:fs/promises";
|
|
45604
|
-
import { join as
|
|
46279
|
+
import { join as join17 } from "node:path";
|
|
45605
46280
|
|
|
45606
46281
|
// src/hostsCli.ts
|
|
45607
46282
|
init_tools_node();
|
|
@@ -45717,16 +46392,16 @@ async function countCrew(crewDir) {
|
|
|
45717
46392
|
return entries.filter((e2) => e2.isFile() && /\.(prmd|md)$/i.test(e2.name)).length;
|
|
45718
46393
|
}
|
|
45719
46394
|
async function countAgents(hostDir) {
|
|
45720
|
-
let total = await countCrew(
|
|
46395
|
+
let total = await countCrew(join17(hostDir, "crew"));
|
|
45721
46396
|
let workspaces;
|
|
45722
46397
|
try {
|
|
45723
|
-
workspaces = await readdir3(
|
|
46398
|
+
workspaces = await readdir3(join17(hostDir, WORKSPACES_SEG), { withFileTypes: true });
|
|
45724
46399
|
} catch {
|
|
45725
46400
|
return total;
|
|
45726
46401
|
}
|
|
45727
46402
|
for (const entry of workspaces) {
|
|
45728
46403
|
if (!entry.isDirectory()) continue;
|
|
45729
|
-
total += await countCrew(
|
|
46404
|
+
total += await countCrew(join17(hostDir, WORKSPACES_SEG, entry.name, "crew"));
|
|
45730
46405
|
}
|
|
45731
46406
|
return total;
|
|
45732
46407
|
}
|
|
@@ -45852,11 +46527,11 @@ function startAccountWatch(opts) {
|
|
|
45852
46527
|
// src/runner/pauseStore.ts
|
|
45853
46528
|
import { promises as fs14 } from "node:fs";
|
|
45854
46529
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
45855
|
-
import { join as
|
|
46530
|
+
import { join as join18, dirname as dirname7 } from "node:path";
|
|
45856
46531
|
var MAX_PAUSES = 1e3;
|
|
45857
46532
|
var MAX_FIELD_LEN2 = 200;
|
|
45858
46533
|
function pausesPath(stateDir) {
|
|
45859
|
-
return
|
|
46534
|
+
return join18(stateDir, "sidecar", "pauses.json");
|
|
45860
46535
|
}
|
|
45861
46536
|
function keyOf2(owner, agent) {
|
|
45862
46537
|
return `${owner}\0${agent}`;
|
|
@@ -45899,9 +46574,9 @@ async function read2(stateDir, fallbackPausedAt) {
|
|
|
45899
46574
|
}
|
|
45900
46575
|
async function write2(stateDir, paused) {
|
|
45901
46576
|
const target = pausesPath(stateDir);
|
|
45902
|
-
const dir =
|
|
46577
|
+
const dir = dirname7(target);
|
|
45903
46578
|
await fs14.mkdir(dir, { recursive: true });
|
|
45904
|
-
const tmp =
|
|
46579
|
+
const tmp = join18(dir, `.pauses.json.tmp-${randomBytes4(8).toString("hex")}`);
|
|
45905
46580
|
const body2 = { version: 1, paused: [...paused] };
|
|
45906
46581
|
try {
|
|
45907
46582
|
await fs14.writeFile(tmp, JSON.stringify(body2, null, 2));
|
|
@@ -46171,10 +46846,10 @@ async function runSessionsCli(argv, deps) {
|
|
|
46171
46846
|
}
|
|
46172
46847
|
if (verb === "create") {
|
|
46173
46848
|
const root = rootFrom(argv, process.cwd());
|
|
46174
|
-
const
|
|
46175
|
-
if (
|
|
46849
|
+
const backend2 = deps.backend ?? backendFor(deps, [root], run);
|
|
46850
|
+
if (backend2.adopt) await backend2.adopt().catch(() => void 0);
|
|
46176
46851
|
try {
|
|
46177
|
-
const handle = await
|
|
46852
|
+
const handle = await backend2.create({ workdir: root, owner: process.env.USERNAME || process.env.USER || "cli" });
|
|
46178
46853
|
out(`${handle.id}
|
|
46179
46854
|
`);
|
|
46180
46855
|
err(`created ${handle.id} on ${handle.workdir}
|
|
@@ -46203,19 +46878,19 @@ async function runSessionsCli(argv, deps) {
|
|
|
46203
46878
|
`);
|
|
46204
46879
|
return 1;
|
|
46205
46880
|
}
|
|
46206
|
-
const
|
|
46207
|
-
if (!
|
|
46881
|
+
const backend2 = deps.backend ?? backendFor(deps, [found.workdir], run);
|
|
46882
|
+
if (!backend2.exec) {
|
|
46208
46883
|
err("corenel sessions run: this backend cannot execute inside a session\n");
|
|
46209
46884
|
return 1;
|
|
46210
46885
|
}
|
|
46211
|
-
if (
|
|
46886
|
+
if (backend2.adopt) await backend2.adopt().catch(() => void 0);
|
|
46212
46887
|
try {
|
|
46213
46888
|
const token = await readAuthToken() ?? process.env.CORENEL_TOKEN ?? null;
|
|
46214
46889
|
if (!token) {
|
|
46215
46890
|
err("corenel sessions run: not signed in on this host.\n Run `corenel login` first \u2014 the session uses the host's credential.\n");
|
|
46216
46891
|
return 1;
|
|
46217
46892
|
}
|
|
46218
|
-
const r2 = await
|
|
46893
|
+
const r2 = await backend2.exec(found.id, ["corenel", "run", prompt], { env: { CORENEL_TOKEN: token } });
|
|
46219
46894
|
if (r2.stdout) out(r2.stdout.endsWith("\n") ? r2.stdout : `${r2.stdout}
|
|
46220
46895
|
`);
|
|
46221
46896
|
if (r2.stderr.trim()) err(r2.stderr.endsWith("\n") ? r2.stderr : `${r2.stderr}
|
|
@@ -46240,9 +46915,9 @@ async function runSessionsCli(argv, deps) {
|
|
|
46240
46915
|
`);
|
|
46241
46916
|
return 1;
|
|
46242
46917
|
}
|
|
46243
|
-
const
|
|
46244
|
-
if (
|
|
46245
|
-
const known = !!
|
|
46918
|
+
const backend2 = deps.backend ?? backendFor(deps, [found.workdir], run);
|
|
46919
|
+
if (backend2.adopt) await backend2.adopt().catch(() => void 0);
|
|
46920
|
+
const known = !!backend2.get?.(found.id);
|
|
46246
46921
|
if (!known) {
|
|
46247
46922
|
const r2 = await sandboxLifecycle(run, verb, found.sandbox);
|
|
46248
46923
|
if (!r2.ok) {
|
|
@@ -46264,7 +46939,7 @@ async function runSessionsCli(argv, deps) {
|
|
|
46264
46939
|
}
|
|
46265
46940
|
try {
|
|
46266
46941
|
if (verb === "pause") {
|
|
46267
|
-
const r2 = await
|
|
46942
|
+
const r2 = await backend2.pause(found.id);
|
|
46268
46943
|
if (r2 && !r2.drained) {
|
|
46269
46944
|
err(`paused ${found.id}, but ${r2.remaining.length} call(s) were STILL RUNNING when the drain gave up
|
|
46270
46945
|
`);
|
|
@@ -46275,12 +46950,12 @@ async function runSessionsCli(argv, deps) {
|
|
|
46275
46950
|
return 0;
|
|
46276
46951
|
}
|
|
46277
46952
|
if (verb === "resume") {
|
|
46278
|
-
await
|
|
46953
|
+
await backend2.resume(found.id);
|
|
46279
46954
|
out(`resumed ${found.id}
|
|
46280
46955
|
`);
|
|
46281
46956
|
return 0;
|
|
46282
46957
|
}
|
|
46283
|
-
await
|
|
46958
|
+
await backend2.destroy(found.id);
|
|
46284
46959
|
const still = (await listSessions(run)).find((r2) => r2.id === found.id);
|
|
46285
46960
|
if (still) {
|
|
46286
46961
|
err(`corenel sessions destroy: ${found.id} is still present (status ${still.status})
|
|
@@ -46704,7 +47379,7 @@ var FLAG_TO_KEY = {
|
|
|
46704
47379
|
"--default-model": "defaultModel",
|
|
46705
47380
|
"--name": "name"
|
|
46706
47381
|
};
|
|
46707
|
-
var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.
|
|
47382
|
+
var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.9.0";
|
|
46708
47383
|
function parseArgsWithProvenance(argv) {
|
|
46709
47384
|
const explicit = /* @__PURE__ */ new Set();
|
|
46710
47385
|
for (const a2 of argv) {
|
|
@@ -46846,78 +47521,6 @@ async function resolveEndpoint(args, env = process.env) {
|
|
|
46846
47521
|
});
|
|
46847
47522
|
}
|
|
46848
47523
|
|
|
46849
|
-
// src/config/daemonConfig.ts
|
|
46850
|
-
import { readFile as readFile3, writeFile, mkdir as mkdir3 } from "node:fs/promises";
|
|
46851
|
-
import { dirname as dirname7, join as join18 } from "node:path";
|
|
46852
|
-
var DAEMON_CONFIG_VERSION = 1;
|
|
46853
|
-
var SECRET_KEYS = ["acpAuth", "tunnelToken", "token"];
|
|
46854
|
-
var KNOWN_KEYS = [
|
|
46855
|
-
"version",
|
|
46856
|
-
"port",
|
|
46857
|
-
"host",
|
|
46858
|
-
"roots",
|
|
46859
|
-
"addUnder",
|
|
46860
|
-
"cloneUnder",
|
|
46861
|
-
"allowLan",
|
|
46862
|
-
"allowOrigins",
|
|
46863
|
-
"tlsCert",
|
|
46864
|
-
"tlsKey",
|
|
46865
|
-
"selfSigned",
|
|
46866
|
-
"tunnel",
|
|
46867
|
-
"tunnelName",
|
|
46868
|
-
"tunnelHostname",
|
|
46869
|
-
"endpoint",
|
|
46870
|
-
"allowShell",
|
|
46871
|
-
"keepTranscripts",
|
|
46872
|
-
"isolation",
|
|
46873
|
-
"msbBin",
|
|
46874
|
-
"guestImage",
|
|
46875
|
-
"maxChainDepth",
|
|
46876
|
-
"name",
|
|
46877
|
-
"appUrl",
|
|
46878
|
-
"defaultModel",
|
|
46879
|
-
"mcpServers",
|
|
46880
|
-
"mcpAllow"
|
|
46881
|
-
];
|
|
46882
|
-
function daemonConfigPath(stateDir) {
|
|
46883
|
-
return join18(stateDir, "daemon.json");
|
|
46884
|
-
}
|
|
46885
|
-
async function readDaemonConfig(stateDir) {
|
|
46886
|
-
const empty = (corrupt) => ({ config: { version: DAEMON_CONFIG_VERSION }, unknown: {}, corrupt });
|
|
46887
|
-
let raw;
|
|
46888
|
-
try {
|
|
46889
|
-
raw = await readFile3(daemonConfigPath(stateDir), "utf8");
|
|
46890
|
-
} catch {
|
|
46891
|
-
return empty(false);
|
|
46892
|
-
}
|
|
46893
|
-
let parsed;
|
|
46894
|
-
try {
|
|
46895
|
-
parsed = JSON.parse(raw);
|
|
46896
|
-
} catch {
|
|
46897
|
-
return empty(true);
|
|
46898
|
-
}
|
|
46899
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return empty(true);
|
|
46900
|
-
const src2 = parsed;
|
|
46901
|
-
const config = { version: DAEMON_CONFIG_VERSION };
|
|
46902
|
-
const unknown = {};
|
|
46903
|
-
for (const [k2, v2] of Object.entries(src2)) {
|
|
46904
|
-
if (k2 === "version") continue;
|
|
46905
|
-
if (KNOWN_KEYS.includes(k2)) {
|
|
46906
|
-
config[k2] = v2;
|
|
46907
|
-
} else {
|
|
46908
|
-
unknown[k2] = v2;
|
|
46909
|
-
}
|
|
46910
|
-
}
|
|
46911
|
-
return { config, unknown, corrupt: false };
|
|
46912
|
-
}
|
|
46913
|
-
async function writeDaemonConfig(stateDir, next, unknown = {}) {
|
|
46914
|
-
const body2 = { ...unknown, ...next, version: DAEMON_CONFIG_VERSION };
|
|
46915
|
-
const path = daemonConfigPath(stateDir);
|
|
46916
|
-
await mkdir3(dirname7(path), { recursive: true });
|
|
46917
|
-
await writeFile(path, `${JSON.stringify(body2, null, 2)}
|
|
46918
|
-
`, "utf8");
|
|
46919
|
-
}
|
|
46920
|
-
|
|
46921
47524
|
// src/config/layer.ts
|
|
46922
47525
|
var STRICT_BOOLEAN_KEYS = /* @__PURE__ */ new Set(["allowShell", "allowLan", "selfSigned", "keepTranscripts"]);
|
|
46923
47526
|
var KEY_MAP = {
|
|
@@ -47344,7 +47947,162 @@ async function runConfig(raw, io) {
|
|
|
47344
47947
|
return 2;
|
|
47345
47948
|
}
|
|
47346
47949
|
|
|
47347
|
-
// src/
|
|
47950
|
+
// ../mcp/src/httpClient.ts
|
|
47951
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
47952
|
+
var CLIENT_INFO = { name: "corenel", version: "0.2.0" };
|
|
47953
|
+
var SESSION_HEADER = "Mcp-Session-Id";
|
|
47954
|
+
var McpHttpClient = class {
|
|
47955
|
+
constructor(server, auth, fetchImpl = fetch) {
|
|
47956
|
+
this.server = server;
|
|
47957
|
+
this.auth = auth;
|
|
47958
|
+
this.fetchImpl = fetchImpl;
|
|
47959
|
+
}
|
|
47960
|
+
nextRequestId = 1;
|
|
47961
|
+
sessionId;
|
|
47962
|
+
async initialize() {
|
|
47963
|
+
await this.send("initialize", {
|
|
47964
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
47965
|
+
capabilities: {},
|
|
47966
|
+
clientInfo: CLIENT_INFO
|
|
47967
|
+
});
|
|
47968
|
+
}
|
|
47969
|
+
async listTools() {
|
|
47970
|
+
const result = await this.send("tools/list", {});
|
|
47971
|
+
const tools = isRecord2(result) ? result.tools : void 0;
|
|
47972
|
+
if (!Array.isArray(tools)) return [];
|
|
47973
|
+
return tools.filter(isMcpTool);
|
|
47974
|
+
}
|
|
47975
|
+
async callTool(name, args) {
|
|
47976
|
+
const result = await this.send("tools/call", { name, arguments: args });
|
|
47977
|
+
const record = isRecord2(result) ? result : {};
|
|
47978
|
+
return { content: record.content, isError: record.isError === true };
|
|
47979
|
+
}
|
|
47980
|
+
/** Send one JSON-RPC request, retrying exactly once on a 401 that the
|
|
47981
|
+
* AuthProvider says it recovered from. `isRetry` prevents a second retry
|
|
47982
|
+
* loop -- a credential that fails twice is refused, not flaky. */
|
|
47983
|
+
async send(method, params, isRetry = false) {
|
|
47984
|
+
const headers = await this.buildHeaders();
|
|
47985
|
+
const id = this.nextRequestId++;
|
|
47986
|
+
const res = await this.request(headers, id, method, params);
|
|
47987
|
+
if (res.status === 401) {
|
|
47988
|
+
if (!isRetry && await this.auth.onUnauthorized()) {
|
|
47989
|
+
return this.send(method, params, true);
|
|
47990
|
+
}
|
|
47991
|
+
throw new Error(
|
|
47992
|
+
`MCP server "${this.server.name}" refused the credential (401) -- run: corenel mcp login ${this.server.name}`
|
|
47993
|
+
);
|
|
47994
|
+
}
|
|
47995
|
+
const sessionId = res.headers.get(SESSION_HEADER);
|
|
47996
|
+
if (sessionId) this.sessionId = sessionId;
|
|
47997
|
+
if (!res.ok) {
|
|
47998
|
+
throw new Error(`MCP server "${this.server.name}" returned HTTP ${res.status}`);
|
|
47999
|
+
}
|
|
48000
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
48001
|
+
let body2;
|
|
48002
|
+
if (contentType.includes("text/event-stream")) {
|
|
48003
|
+
body2 = await this.readSseResponse(res, id);
|
|
48004
|
+
} else if (contentType.includes("application/json")) {
|
|
48005
|
+
body2 = await res.json();
|
|
48006
|
+
} else {
|
|
48007
|
+
throw new Error(
|
|
48008
|
+
`MCP server "${this.server.name}" answered with an unexpected content-type: "${contentType || "(none)"}"`
|
|
48009
|
+
);
|
|
48010
|
+
}
|
|
48011
|
+
if (body2.error) {
|
|
48012
|
+
throw new Error(`MCP server "${this.server.name}" returned error ${body2.error.code}: ${body2.error.message}`);
|
|
48013
|
+
}
|
|
48014
|
+
return body2.result;
|
|
48015
|
+
}
|
|
48016
|
+
/** Read an SSE body until the JSON-RPC response for `id` arrives.
|
|
48017
|
+
*
|
|
48018
|
+
* Events are separated by a blank line; an event's `data:` lines are joined
|
|
48019
|
+
* with newlines, per the SSE spec, and parsed as one JSON-RPC message.
|
|
48020
|
+
* Notifications (no `id`) and responses to other ids are skipped. The reader
|
|
48021
|
+
* is cancelled once the answer is in, so a server that keeps the stream open
|
|
48022
|
+
* after replying cannot hold this call open with it. */
|
|
48023
|
+
async readSseResponse(res, id) {
|
|
48024
|
+
if (!res.body) {
|
|
48025
|
+
throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
|
|
48026
|
+
}
|
|
48027
|
+
const reader = res.body.getReader();
|
|
48028
|
+
const decoder = new TextDecoder();
|
|
48029
|
+
let buffer = "";
|
|
48030
|
+
try {
|
|
48031
|
+
for (; ; ) {
|
|
48032
|
+
const { done, value } = await reader.read();
|
|
48033
|
+
if (value) buffer += decoder.decode(value, { stream: true });
|
|
48034
|
+
buffer = buffer.replace(/\r\n/g, "\n");
|
|
48035
|
+
let boundary = buffer.indexOf("\n\n");
|
|
48036
|
+
while (boundary !== -1) {
|
|
48037
|
+
const event = buffer.slice(0, boundary);
|
|
48038
|
+
buffer = buffer.slice(boundary + 2);
|
|
48039
|
+
const match = this.matchSseEvent(event, id);
|
|
48040
|
+
if (match) return match;
|
|
48041
|
+
boundary = buffer.indexOf("\n\n");
|
|
48042
|
+
}
|
|
48043
|
+
if (done) {
|
|
48044
|
+
const tail3 = buffer.trim() ? this.matchSseEvent(buffer, id) : null;
|
|
48045
|
+
if (tail3) return tail3;
|
|
48046
|
+
throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
|
|
48047
|
+
}
|
|
48048
|
+
}
|
|
48049
|
+
} finally {
|
|
48050
|
+
reader.cancel().catch(() => {
|
|
48051
|
+
});
|
|
48052
|
+
}
|
|
48053
|
+
}
|
|
48054
|
+
/** The JSON-RPC response in one SSE event, if it answers `id`; else null. */
|
|
48055
|
+
matchSseEvent(event, id) {
|
|
48056
|
+
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
48057
|
+
if (!data) return null;
|
|
48058
|
+
let parsed;
|
|
48059
|
+
try {
|
|
48060
|
+
parsed = JSON.parse(data);
|
|
48061
|
+
} catch {
|
|
48062
|
+
return null;
|
|
48063
|
+
}
|
|
48064
|
+
if (!isRecord2(parsed) || parsed.id !== id) return null;
|
|
48065
|
+
if (!("result" in parsed) && !("error" in parsed)) return null;
|
|
48066
|
+
return parsed;
|
|
48067
|
+
}
|
|
48068
|
+
/** The bare `fetch()` call, wrapped -- every OTHER throw in `send` already
|
|
48069
|
+
* names the server; this is the one call whose own failures (DNS miss,
|
|
48070
|
+
* connection refused, TLS failure) never reach `send`'s response-handling
|
|
48071
|
+
* at all. Left unguarded, node's bare `fetch failed` -- naming neither the
|
|
48072
|
+
* URL nor the actual cause -- propagates straight out of `send`, into
|
|
48073
|
+
* `registry.ts`'s `status().error`, and out of `doctor`, the one command
|
|
48074
|
+
* whose entire job is diagnosis. `err.cause` carries the real reason
|
|
48075
|
+
* (ECONNREFUSED, ENOTFOUND, a TLS alert); undici puts it there rather than
|
|
48076
|
+
* in `err.message`. */
|
|
48077
|
+
async request(headers, id, method, params) {
|
|
48078
|
+
try {
|
|
48079
|
+
return await this.fetchImpl(this.server.url, {
|
|
48080
|
+
method: "POST",
|
|
48081
|
+
headers,
|
|
48082
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
48083
|
+
});
|
|
48084
|
+
} catch (err) {
|
|
48085
|
+
const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : err instanceof Error ? err.message : String(err);
|
|
48086
|
+
throw new Error(`MCP server "${this.server.name}" (${this.server.url}) is unreachable: ${cause}`);
|
|
48087
|
+
}
|
|
48088
|
+
}
|
|
48089
|
+
async buildHeaders() {
|
|
48090
|
+
const headers = new Headers();
|
|
48091
|
+
headers.set("Content-Type", "application/json");
|
|
48092
|
+
headers.set("Accept", "application/json, text/event-stream");
|
|
48093
|
+
for (const h2 of await this.auth.headers()) headers.set(h2.name, h2.value);
|
|
48094
|
+
if (this.sessionId) headers.set(SESSION_HEADER, this.sessionId);
|
|
48095
|
+
return headers;
|
|
48096
|
+
}
|
|
48097
|
+
};
|
|
48098
|
+
function isRecord2(value) {
|
|
48099
|
+
return typeof value === "object" && value !== null;
|
|
48100
|
+
}
|
|
48101
|
+
function isMcpTool(value) {
|
|
48102
|
+
return isRecord2(value) && typeof value.name === "string" && "inputSchema" in value;
|
|
48103
|
+
}
|
|
48104
|
+
|
|
48105
|
+
// ../mcp/src/config.ts
|
|
47348
48106
|
init_tools_node();
|
|
47349
48107
|
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir4, chmod } from "node:fs/promises";
|
|
47350
48108
|
function isUsableServer(v2) {
|
|
@@ -47463,7 +48221,7 @@ async function writeMcpCredential(stateDir, server, cred) {
|
|
|
47463
48221
|
}
|
|
47464
48222
|
}
|
|
47465
48223
|
|
|
47466
|
-
// src/
|
|
48224
|
+
// ../mcp/src/oauth/discovery.ts
|
|
47467
48225
|
var PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
|
47468
48226
|
var AUTH_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
47469
48227
|
async function tryFetchJson(url, fetchImpl) {
|
|
@@ -47561,7 +48319,423 @@ async function discoverAuthMetadata(authServer, fetchImpl = fetch) {
|
|
|
47561
48319
|
return { authorizationEndpoint, tokenEndpoint, registrationEndpoint, scopesSupported };
|
|
47562
48320
|
}
|
|
47563
48321
|
|
|
47564
|
-
// src/
|
|
48322
|
+
// ../mcp/src/oauth/tokens.ts
|
|
48323
|
+
function parseTokenResponse(body2) {
|
|
48324
|
+
if (typeof body2 !== "object" || body2 === null || Array.isArray(body2)) {
|
|
48325
|
+
throw new Error("token endpoint returned a malformed response");
|
|
48326
|
+
}
|
|
48327
|
+
const r2 = body2;
|
|
48328
|
+
if (typeof r2.access_token !== "string" || r2.access_token.length === 0) {
|
|
48329
|
+
throw new Error("token endpoint response did not include an access_token");
|
|
48330
|
+
}
|
|
48331
|
+
return {
|
|
48332
|
+
access_token: r2.access_token,
|
|
48333
|
+
...typeof r2.refresh_token === "string" ? { refresh_token: r2.refresh_token } : {},
|
|
48334
|
+
...typeof r2.expires_in === "number" ? { expires_in: r2.expires_in } : {},
|
|
48335
|
+
...typeof r2.scope === "string" ? { scope: r2.scope } : {}
|
|
48336
|
+
};
|
|
48337
|
+
}
|
|
48338
|
+
async function postToken(endpoint, body2, fetchImpl) {
|
|
48339
|
+
const res = await fetchImpl(endpoint, {
|
|
48340
|
+
method: "POST",
|
|
48341
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
48342
|
+
body: body2.toString()
|
|
48343
|
+
});
|
|
48344
|
+
if (!res.ok) {
|
|
48345
|
+
throw new Error(`token request failed at ${endpoint} (HTTP ${res.status})`);
|
|
48346
|
+
}
|
|
48347
|
+
return parseTokenResponse(await res.json());
|
|
48348
|
+
}
|
|
48349
|
+
async function exchangeCode(meta, opts, fetchImpl = fetch, now2 = Date.now) {
|
|
48350
|
+
const body2 = new URLSearchParams({
|
|
48351
|
+
grant_type: "authorization_code",
|
|
48352
|
+
client_id: opts.clientId,
|
|
48353
|
+
code: opts.code,
|
|
48354
|
+
code_verifier: opts.verifier,
|
|
48355
|
+
redirect_uri: opts.redirectUri,
|
|
48356
|
+
resource: opts.resource
|
|
48357
|
+
});
|
|
48358
|
+
if (opts.clientSecret) body2.set("client_secret", opts.clientSecret);
|
|
48359
|
+
const tokens = await postToken(meta.tokenEndpoint, body2, fetchImpl);
|
|
48360
|
+
return {
|
|
48361
|
+
kind: "oauth",
|
|
48362
|
+
accessToken: tokens.access_token,
|
|
48363
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {},
|
|
48364
|
+
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
48365
|
+
clientId: opts.clientId,
|
|
48366
|
+
...opts.clientSecret ? { clientSecret: opts.clientSecret } : {},
|
|
48367
|
+
authServer: opts.authServer,
|
|
48368
|
+
...tokens.scope ? { scope: tokens.scope } : {}
|
|
48369
|
+
};
|
|
48370
|
+
}
|
|
48371
|
+
async function refresh(meta, cred, fetchImpl = fetch, now2 = Date.now) {
|
|
48372
|
+
if (!cred.refreshToken) {
|
|
48373
|
+
throw new Error("credential has no refresh token to refresh with -- sign in again");
|
|
48374
|
+
}
|
|
48375
|
+
const body2 = new URLSearchParams({
|
|
48376
|
+
grant_type: "refresh_token",
|
|
48377
|
+
refresh_token: cred.refreshToken,
|
|
48378
|
+
client_id: cred.clientId
|
|
48379
|
+
});
|
|
48380
|
+
if (cred.clientSecret) body2.set("client_secret", cred.clientSecret);
|
|
48381
|
+
const tokens = await postToken(meta.tokenEndpoint, body2, fetchImpl);
|
|
48382
|
+
return {
|
|
48383
|
+
kind: "oauth",
|
|
48384
|
+
accessToken: tokens.access_token,
|
|
48385
|
+
// Keep the old refresh token when the server did not send a new one.
|
|
48386
|
+
refreshToken: tokens.refresh_token ?? cred.refreshToken,
|
|
48387
|
+
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
48388
|
+
clientId: cred.clientId,
|
|
48389
|
+
...cred.clientSecret ? { clientSecret: cred.clientSecret } : {},
|
|
48390
|
+
authServer: cred.authServer,
|
|
48391
|
+
scope: tokens.scope ?? cred.scope
|
|
48392
|
+
};
|
|
48393
|
+
}
|
|
48394
|
+
var EXPIRY_SKEW_MS = 6e4;
|
|
48395
|
+
function isExpired(cred, nowMs) {
|
|
48396
|
+
if (cred.expiresAt === void 0) return false;
|
|
48397
|
+
return cred.expiresAt - nowMs <= EXPIRY_SKEW_MS;
|
|
48398
|
+
}
|
|
48399
|
+
|
|
48400
|
+
// ../mcp/src/auth.ts
|
|
48401
|
+
function authProviderFor(stateDir, server, fetchImpl = fetch, accountToken = async () => null) {
|
|
48402
|
+
async function doRefresh(cred) {
|
|
48403
|
+
if (!cred.refreshToken) {
|
|
48404
|
+
throw new Error(`"${server.name}" has no refresh token on file -- run: corenel mcp login ${server.name}`);
|
|
48405
|
+
}
|
|
48406
|
+
const meta = await discoverAuthMetadata(cred.authServer, fetchImpl);
|
|
48407
|
+
const refreshed = await refresh(meta, cred, fetchImpl);
|
|
48408
|
+
await writeMcpCredential(stateDir, server.name, refreshed);
|
|
48409
|
+
return refreshed;
|
|
48410
|
+
}
|
|
48411
|
+
return {
|
|
48412
|
+
async headers() {
|
|
48413
|
+
const cred = await readMcpCredential(stateDir, server.name);
|
|
48414
|
+
if (!cred && !server.hosted) {
|
|
48415
|
+
throw new Error(`No credential stored for MCP server "${server.name}" -- run: corenel mcp login ${server.name} (or set-header, for a static token).`);
|
|
48416
|
+
}
|
|
48417
|
+
const mcpHeaders = !cred ? [] : cred.kind === "headers" ? cred.headers : [{ name: "Authorization", value: `Bearer ${(isExpired(cred, Date.now()) ? await doRefresh(cred) : cred).accessToken}` }];
|
|
48418
|
+
if (!server.hosted) return mcpHeaders;
|
|
48419
|
+
const moved = mcpHeaders.map((h2) => h2.name.toLowerCase() === "authorization" ? { name: "X-MCP-Authorization", value: h2.value } : h2);
|
|
48420
|
+
const account = await accountToken();
|
|
48421
|
+
return account ? [...moved, { name: "Authorization", value: `Bearer ${account}` }] : moved;
|
|
48422
|
+
},
|
|
48423
|
+
async onUnauthorized() {
|
|
48424
|
+
const cred = await readMcpCredential(stateDir, server.name);
|
|
48425
|
+
if (!cred || cred.kind !== "oauth") return false;
|
|
48426
|
+
try {
|
|
48427
|
+
await doRefresh(cred);
|
|
48428
|
+
return true;
|
|
48429
|
+
} catch {
|
|
48430
|
+
return false;
|
|
48431
|
+
}
|
|
48432
|
+
}
|
|
48433
|
+
};
|
|
48434
|
+
}
|
|
48435
|
+
|
|
48436
|
+
// ../mcp/src/timedFetch.ts
|
|
48437
|
+
var AUTH_REQUEST_TIMEOUT_MS = 15e3;
|
|
48438
|
+
function timedFetch(ms, inner = fetch) {
|
|
48439
|
+
return async (input, init2 = {}) => {
|
|
48440
|
+
const deadline = AbortSignal.timeout(ms);
|
|
48441
|
+
const signal = init2.signal ? AbortSignal.any([init2.signal, deadline]) : deadline;
|
|
48442
|
+
try {
|
|
48443
|
+
return await inner(input, { ...init2, signal });
|
|
48444
|
+
} catch (err) {
|
|
48445
|
+
if (deadline.aborted && !init2.signal?.aborted) {
|
|
48446
|
+
throw new Error(`no answer within ${ms / 1e3}s`, { cause: err });
|
|
48447
|
+
}
|
|
48448
|
+
throw err;
|
|
48449
|
+
}
|
|
48450
|
+
};
|
|
48451
|
+
}
|
|
48452
|
+
|
|
48453
|
+
// ../mcp/src/registry.ts
|
|
48454
|
+
async function accountTokenFor(stateDir) {
|
|
48455
|
+
try {
|
|
48456
|
+
const cred = await resolveCredential(process.env, stateDir);
|
|
48457
|
+
return cred.token ?? null;
|
|
48458
|
+
} catch {
|
|
48459
|
+
return null;
|
|
48460
|
+
}
|
|
48461
|
+
}
|
|
48462
|
+
function defaultClientFactory(stateDir, server) {
|
|
48463
|
+
return new McpHttpClient(
|
|
48464
|
+
server,
|
|
48465
|
+
authProviderFor(stateDir, server, timedFetch(AUTH_REQUEST_TIMEOUT_MS), () => accountTokenFor(stateDir))
|
|
48466
|
+
);
|
|
48467
|
+
}
|
|
48468
|
+
var SERVER_CONNECT_TIMEOUT_MS = 3e4;
|
|
48469
|
+
var McpRegistry = class {
|
|
48470
|
+
constructor(stateDir, clientFactory = defaultClientFactory) {
|
|
48471
|
+
this.stateDir = stateDir;
|
|
48472
|
+
this.clientFactory = clientFactory;
|
|
48473
|
+
}
|
|
48474
|
+
connected = /* @__PURE__ */ new Map();
|
|
48475
|
+
statuses = /* @__PURE__ */ new Map();
|
|
48476
|
+
// The order the operator wrote servers in daemon.json, captured once per
|
|
48477
|
+
// connectAll() -- `tools()` walks THIS, never `connected`'s Map iteration
|
|
48478
|
+
// order. `connectOne` runs concurrently for every server (Promise.all), so
|
|
48479
|
+
// Map insertion order is race order: whichever server's initialize+list
|
|
48480
|
+
// happened to resolve first. That is fine for `connected` (a lookup table,
|
|
48481
|
+
// order never read) but `tools()` is a list a model/doctor/policy consumes
|
|
48482
|
+
// in order, and race order means a different tool ordering on every daemon
|
|
48483
|
+
// boot -- exactly the kind of bug that surfaces far from here and is never
|
|
48484
|
+
// traced back. Configured order is also information the operator supplied
|
|
48485
|
+
// deliberately; sorting alphabetically would discard it.
|
|
48486
|
+
serverOrder = [];
|
|
48487
|
+
// Tool lists seeded from the on-disk cache, with no socket ever opened.
|
|
48488
|
+
// Deliberately a THIRD piece of state, not folded into `connected`: a
|
|
48489
|
+
// primed server can answer `tools()` but must still go through
|
|
48490
|
+
// `ensureConnected` before `call()` will dispatch to it. `connectAll`
|
|
48491
|
+
// does not touch this map -- see the comment at its first line for why.
|
|
48492
|
+
primed = /* @__PURE__ */ new Map();
|
|
48493
|
+
// One in-flight ensureConnected() attempt per server, so three tool calls
|
|
48494
|
+
// that land on the same cold server in the same turn share one connection
|
|
48495
|
+
// instead of racing three.
|
|
48496
|
+
connecting = /* @__PURE__ */ new Map();
|
|
48497
|
+
/**
|
|
48498
|
+
* Connect to every configured server and cache its tools. Never throws --
|
|
48499
|
+
* a server that fails to initialize, or fails listing tools, is recorded
|
|
48500
|
+
* in `status()` with zero tools and the daemon keeps starting.
|
|
48501
|
+
*/
|
|
48502
|
+
async connectAll(opts = {}) {
|
|
48503
|
+
this.connected.clear();
|
|
48504
|
+
this.statuses.clear();
|
|
48505
|
+
this.primed.clear();
|
|
48506
|
+
const servers = await readMcpServers(this.stateDir);
|
|
48507
|
+
this.serverOrder = servers.map((s2) => s2.name);
|
|
48508
|
+
const timeoutMs = opts.timeoutMs ?? SERVER_CONNECT_TIMEOUT_MS;
|
|
48509
|
+
await Promise.all(servers.map((server) => this.connectOne(server, timeoutMs, opts.onServer)));
|
|
48510
|
+
}
|
|
48511
|
+
/* BOUNDED, AND SETTLED EXACTLY ONCE.
|
|
48512
|
+
*
|
|
48513
|
+
* Every server connects under one Promise.all, and daemon startup awaits it.
|
|
48514
|
+
* With no bound, one stalled server -- measured: an expired Microsoft 365
|
|
48515
|
+
* token refreshing against a cold service, still waiting at 60 seconds --
|
|
48516
|
+
* left the terminal silent after `mirrors sessions`, indistinguishable from a
|
|
48517
|
+
* crash.
|
|
48518
|
+
*
|
|
48519
|
+
* The deadline covers the whole connect (any credential refresh, initialize,
|
|
48520
|
+
* tools/list), not each request, because several individually bounded steps
|
|
48521
|
+
* in a row can still add up to a hang. The attempt that loses the race keeps
|
|
48522
|
+
* running; its late result is deliberately ignored, so a server reported as
|
|
48523
|
+
* timed out cannot quietly become connected afterwards -- status would then
|
|
48524
|
+
* tell doctor one thing and the registered tools another. */
|
|
48525
|
+
async connectOne(server, timeoutMs, onServer) {
|
|
48526
|
+
let timer;
|
|
48527
|
+
const deadline = new Promise((resolve14) => {
|
|
48528
|
+
timer = setTimeout(() => resolve14("timeout"), timeoutMs);
|
|
48529
|
+
});
|
|
48530
|
+
const attempt = (async () => {
|
|
48531
|
+
const client = this.clientFactory(this.stateDir, server);
|
|
48532
|
+
await client.initialize();
|
|
48533
|
+
const tools = await client.listTools();
|
|
48534
|
+
return { client, tools };
|
|
48535
|
+
})();
|
|
48536
|
+
attempt.catch(() => {
|
|
48537
|
+
});
|
|
48538
|
+
try {
|
|
48539
|
+
const outcome = await Promise.race([attempt, deadline]);
|
|
48540
|
+
if (outcome === "timeout") {
|
|
48541
|
+
this.statuses.set(server.name, { connected: false, toolCount: 0, error: `timed out after ${timeoutMs / 1e3}s` });
|
|
48542
|
+
} else {
|
|
48543
|
+
this.connected.set(server.name, outcome);
|
|
48544
|
+
this.statuses.set(server.name, { connected: true, toolCount: outcome.tools.length });
|
|
48545
|
+
}
|
|
48546
|
+
} catch (err) {
|
|
48547
|
+
this.statuses.set(server.name, {
|
|
48548
|
+
connected: false,
|
|
48549
|
+
toolCount: 0,
|
|
48550
|
+
error: err instanceof Error ? err.message : String(err)
|
|
48551
|
+
});
|
|
48552
|
+
} finally {
|
|
48553
|
+
clearTimeout(timer);
|
|
48554
|
+
}
|
|
48555
|
+
const settled = this.statuses.get(server.name);
|
|
48556
|
+
if (settled) onServer?.({ server: server.name, ...settled });
|
|
48557
|
+
}
|
|
48558
|
+
/** Every tool from every connected OR primed server, paired with the server
|
|
48559
|
+
* it came from, in CONFIGURED order (`serverOrder`) -- not the order
|
|
48560
|
+
* servers happened to finish connecting in, and not insertion order into
|
|
48561
|
+
* `primed` either. Within a server, tools stay in the order that server
|
|
48562
|
+
* returned them. A live connection wins over a primed (cached) entry for
|
|
48563
|
+
* the same server, since it is the fresher truth. A server with neither
|
|
48564
|
+
* either failed to connect or reported zero tools -- `status()` tells the
|
|
48565
|
+
* two apart. */
|
|
48566
|
+
tools() {
|
|
48567
|
+
const out = [];
|
|
48568
|
+
for (const server of this.serverOrder) {
|
|
48569
|
+
const entry = this.connected.get(server);
|
|
48570
|
+
const list = entry ? entry.tools : this.primed.get(server);
|
|
48571
|
+
if (!list) continue;
|
|
48572
|
+
for (const tool of list) out.push({ server, tool });
|
|
48573
|
+
}
|
|
48574
|
+
return out;
|
|
48575
|
+
}
|
|
48576
|
+
/** Seed tool lists from the on-disk cache. NO I/O, no sockets -- this is
|
|
48577
|
+
* what a terminal calls at startup so that `tools()` can answer
|
|
48578
|
+
* immediately, before any server has actually been reached. A primed
|
|
48579
|
+
* server is deliberately NOT `connected`: it can answer what tools exist,
|
|
48580
|
+
* and cannot call one until `ensureConnected()` has run. */
|
|
48581
|
+
primeFromCache(cache) {
|
|
48582
|
+
for (const [name, entry] of Object.entries(cache)) {
|
|
48583
|
+
this.statuses.set(name, { connected: false, toolCount: entry.tools.length });
|
|
48584
|
+
this.primed.set(name, entry.tools);
|
|
48585
|
+
if (!this.serverOrder.includes(name)) this.serverOrder.push(name);
|
|
48586
|
+
}
|
|
48587
|
+
}
|
|
48588
|
+
isConnected(server) {
|
|
48589
|
+
return this.connected.has(server);
|
|
48590
|
+
}
|
|
48591
|
+
connectedServers() {
|
|
48592
|
+
return this.serverOrder.filter((s2) => this.connected.has(s2));
|
|
48593
|
+
}
|
|
48594
|
+
/** Connect ONE server, on demand. Never throws, for the same reason
|
|
48595
|
+
* connectAll never throws: a server that is down must not take the
|
|
48596
|
+
* process with it. Concurrent callers for the same server share one
|
|
48597
|
+
* attempt rather than opening a second socket. */
|
|
48598
|
+
async ensureConnected(server) {
|
|
48599
|
+
if (this.connected.has(server)) return;
|
|
48600
|
+
const inflight = this.connecting.get(server);
|
|
48601
|
+
if (inflight) return inflight;
|
|
48602
|
+
const attempt = (async () => {
|
|
48603
|
+
const servers = await readMcpServers(this.stateDir);
|
|
48604
|
+
const def = servers.find((s2) => s2.name === server);
|
|
48605
|
+
if (!def) return;
|
|
48606
|
+
if (!this.serverOrder.includes(server)) this.serverOrder.push(server);
|
|
48607
|
+
await this.connectOne(def, SERVER_CONNECT_TIMEOUT_MS);
|
|
48608
|
+
})().finally(() => this.connecting.delete(server));
|
|
48609
|
+
this.connecting.set(server, attempt);
|
|
48610
|
+
return attempt;
|
|
48611
|
+
}
|
|
48612
|
+
/**
|
|
48613
|
+
* Call one tool on one server. Fails immediately, naming the server, when
|
|
48614
|
+
* that server never connected -- there is nothing to route the call
|
|
48615
|
+
* through, and pretending otherwise would either stall waiting on a
|
|
48616
|
+
* connection that will never appear or return an empty result the caller
|
|
48617
|
+
* could mistake for a genuine (if empty) tool response.
|
|
48618
|
+
*/
|
|
48619
|
+
async call(server, tool, args) {
|
|
48620
|
+
await this.ensureConnected(server);
|
|
48621
|
+
const entry = this.connected.get(server);
|
|
48622
|
+
if (!entry) {
|
|
48623
|
+
throw new Error(`MCP server "${server}" is not connected -- run: corenel mcp doctor`);
|
|
48624
|
+
}
|
|
48625
|
+
return entry.client.callTool(tool, args);
|
|
48626
|
+
}
|
|
48627
|
+
/** Per-server connection state, for `doctor` (Task 10) to print. A server
|
|
48628
|
+
* with `connected: false` always carries `error` -- there is no code path
|
|
48629
|
+
* that records a failure without a reason, because "no tools" alone tells
|
|
48630
|
+
* the reader nothing about what to fix. */
|
|
48631
|
+
status() {
|
|
48632
|
+
const out = [];
|
|
48633
|
+
for (const server of this.serverOrder) {
|
|
48634
|
+
const s2 = this.statuses.get(server);
|
|
48635
|
+
if (s2) out.push({ server, ...s2 });
|
|
48636
|
+
}
|
|
48637
|
+
return out;
|
|
48638
|
+
}
|
|
48639
|
+
};
|
|
48640
|
+
|
|
48641
|
+
// ../mcp/src/policy.ts
|
|
48642
|
+
function mcpCallAllowed(address, policy) {
|
|
48643
|
+
const allow = policy.tools?.allow ?? [];
|
|
48644
|
+
if (!matchesAnyGlob(address, allow)) {
|
|
48645
|
+
return { ok: false, reason: `${address} is not allowed - add it with: corenel config add mcpAllow ${address}` };
|
|
48646
|
+
}
|
|
48647
|
+
const service = new PermissionService({
|
|
48648
|
+
policy,
|
|
48649
|
+
defaultForReadOnly: "allow",
|
|
48650
|
+
defaultForMutating: "allow"
|
|
48651
|
+
});
|
|
48652
|
+
const level = service.resolveLevel({ name: address, address, mutates: true });
|
|
48653
|
+
if (level !== "allow") {
|
|
48654
|
+
return {
|
|
48655
|
+
ok: false,
|
|
48656
|
+
reason: `${address} resolves to permission "${level}" - MCP tools never prompt; set it to "allow" or remove the rule`
|
|
48657
|
+
};
|
|
48658
|
+
}
|
|
48659
|
+
return { ok: true };
|
|
48660
|
+
}
|
|
48661
|
+
|
|
48662
|
+
// ../mcp/src/toolNames.ts
|
|
48663
|
+
init_protocol();
|
|
48664
|
+
|
|
48665
|
+
// ../mcp/src/asAgentTools.ts
|
|
48666
|
+
function isPlainObject3(v2) {
|
|
48667
|
+
return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
|
|
48668
|
+
}
|
|
48669
|
+
function toParameters(inputSchema) {
|
|
48670
|
+
if (isPlainObject3(inputSchema)) return inputSchema;
|
|
48671
|
+
return { type: "object", properties: {} };
|
|
48672
|
+
}
|
|
48673
|
+
function formatContent(content) {
|
|
48674
|
+
if (typeof content === "string") return content;
|
|
48675
|
+
if (Array.isArray(content)) {
|
|
48676
|
+
const parts = content.map((block) => {
|
|
48677
|
+
if (isPlainObject3(block) && typeof block.text === "string") return block.text;
|
|
48678
|
+
return JSON.stringify(block);
|
|
48679
|
+
});
|
|
48680
|
+
return parts.join("\n");
|
|
48681
|
+
}
|
|
48682
|
+
if (content == null) return "";
|
|
48683
|
+
return JSON.stringify(content);
|
|
48684
|
+
}
|
|
48685
|
+
function asAgentTools(registry2) {
|
|
48686
|
+
const out = [];
|
|
48687
|
+
for (const { server, tool } of registry2.tools()) {
|
|
48688
|
+
if (!canEncodeMcpWireName(server, tool.name)) {
|
|
48689
|
+
console.error(
|
|
48690
|
+
`MCP tool refused: server "${server}" tool "${tool.name}" cannot be encoded as a wire name (unsupported characters or too long) -- this tool is not available to the model`
|
|
48691
|
+
);
|
|
48692
|
+
continue;
|
|
48693
|
+
}
|
|
48694
|
+
out.push(toAgentTool(registry2, server, tool));
|
|
48695
|
+
}
|
|
48696
|
+
return out;
|
|
48697
|
+
}
|
|
48698
|
+
function toAgentTool(registry2, server, tool) {
|
|
48699
|
+
const name = mcpWireName(server, tool.name);
|
|
48700
|
+
const address = mcpToolAddress(server, tool.name);
|
|
48701
|
+
return {
|
|
48702
|
+
name,
|
|
48703
|
+
namespace: "mcp",
|
|
48704
|
+
description: tool.description?.trim() || `MCP tool "${tool.name}" on server "${server}" (${address}).`,
|
|
48705
|
+
parameters: toParameters(tool.inputSchema),
|
|
48706
|
+
mutates: true,
|
|
48707
|
+
async run(args) {
|
|
48708
|
+
const result = await registry2.call(server, tool.name, args);
|
|
48709
|
+
const text = formatContent(result.content);
|
|
48710
|
+
return result.isError ? `Error: ${text}` : text;
|
|
48711
|
+
}
|
|
48712
|
+
};
|
|
48713
|
+
}
|
|
48714
|
+
|
|
48715
|
+
// ../mcp/src/gatedAgentTools.ts
|
|
48716
|
+
function gatedAgentTools(registry2, policy) {
|
|
48717
|
+
const base = asAgentTools(registry2);
|
|
48718
|
+
const byWireName = new Map(base.map((t2) => [t2.name, t2]));
|
|
48719
|
+
const wrapped = [];
|
|
48720
|
+
for (const { server, tool } of registry2.tools()) {
|
|
48721
|
+
if (!canEncodeMcpWireName(server, tool.name)) continue;
|
|
48722
|
+
const name = mcpWireName(server, tool.name);
|
|
48723
|
+
const original = byWireName.get(name);
|
|
48724
|
+
if (!original) continue;
|
|
48725
|
+
const address = mcpToolAddress(server, tool.name);
|
|
48726
|
+
wrapped.push({
|
|
48727
|
+
...original,
|
|
48728
|
+
async run(args, ctx) {
|
|
48729
|
+
const decision = mcpCallAllowed(address, policy);
|
|
48730
|
+
if (!decision.ok) return `Error: ${decision.reason}`;
|
|
48731
|
+
return original.run(args, ctx);
|
|
48732
|
+
}
|
|
48733
|
+
});
|
|
48734
|
+
}
|
|
48735
|
+
return wrapped;
|
|
48736
|
+
}
|
|
48737
|
+
|
|
48738
|
+
// ../mcp/src/oauth/register.ts
|
|
47565
48739
|
async function registerClient(meta, redirectUri, fetchImpl) {
|
|
47566
48740
|
if (!meta.registrationEndpoint) return null;
|
|
47567
48741
|
const res = await fetchImpl(meta.registrationEndpoint, {
|
|
@@ -47591,7 +48765,7 @@ async function registerClient(meta, redirectUri, fetchImpl) {
|
|
|
47591
48765
|
return { clientId, clientSecret };
|
|
47592
48766
|
}
|
|
47593
48767
|
|
|
47594
|
-
// src/
|
|
48768
|
+
// ../mcp/src/oauth/pkce.ts
|
|
47595
48769
|
import { randomBytes as randomBytes5, createHash as createHash3 } from "node:crypto";
|
|
47596
48770
|
function base64url(bytes) {
|
|
47597
48771
|
return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
@@ -47619,7 +48793,7 @@ function authorizationUrl(meta, opts) {
|
|
|
47619
48793
|
return url.toString();
|
|
47620
48794
|
}
|
|
47621
48795
|
|
|
47622
|
-
// src/
|
|
48796
|
+
// ../mcp/src/oauth/loopback.ts
|
|
47623
48797
|
import { createServer } from "node:http";
|
|
47624
48798
|
var PAGE_OK = `<!doctype html><meta charset="utf-8"><title>Signed in</title>
|
|
47625
48799
|
<body style="font:16px system-ui;padding:3rem;text-align:center">
|
|
@@ -47672,6 +48846,17 @@ function awaitRedirect(port, expectedState, opts = {}) {
|
|
|
47672
48846
|
server.once("error", (e2) => {
|
|
47673
48847
|
finish(() => reject(e2 instanceof Error ? e2 : new Error(String(e2))));
|
|
47674
48848
|
});
|
|
48849
|
+
if (opts.signal) {
|
|
48850
|
+
if (opts.signal.aborted) {
|
|
48851
|
+
finish(() => reject(new Error("sign-in cancelled")));
|
|
48852
|
+
} else {
|
|
48853
|
+
opts.signal.addEventListener(
|
|
48854
|
+
"abort",
|
|
48855
|
+
() => finish(() => reject(new Error("sign-in cancelled"))),
|
|
48856
|
+
{ once: true }
|
|
48857
|
+
);
|
|
48858
|
+
}
|
|
48859
|
+
}
|
|
47675
48860
|
v6.on("error", () => {
|
|
47676
48861
|
});
|
|
47677
48862
|
try {
|
|
@@ -47684,6 +48869,15 @@ function awaitRedirect(port, expectedState, opts = {}) {
|
|
|
47684
48869
|
});
|
|
47685
48870
|
});
|
|
47686
48871
|
}
|
|
48872
|
+
function portFree(port) {
|
|
48873
|
+
return new Promise((resolve14) => {
|
|
48874
|
+
const probe = createServer();
|
|
48875
|
+
probe.once("error", () => resolve14(false));
|
|
48876
|
+
probe.listen(port, "127.0.0.1", () => {
|
|
48877
|
+
probe.close(() => resolve14(true));
|
|
48878
|
+
});
|
|
48879
|
+
});
|
|
48880
|
+
}
|
|
47687
48881
|
function getFreePort() {
|
|
47688
48882
|
return new Promise((resolve14, reject) => {
|
|
47689
48883
|
const probe = createServer();
|
|
@@ -47695,93 +48889,16 @@ function getFreePort() {
|
|
|
47695
48889
|
});
|
|
47696
48890
|
}
|
|
47697
48891
|
|
|
47698
|
-
// src/
|
|
47699
|
-
function parseTokenResponse(body2) {
|
|
47700
|
-
if (typeof body2 !== "object" || body2 === null || Array.isArray(body2)) {
|
|
47701
|
-
throw new Error("token endpoint returned a malformed response");
|
|
47702
|
-
}
|
|
47703
|
-
const r2 = body2;
|
|
47704
|
-
if (typeof r2.access_token !== "string" || r2.access_token.length === 0) {
|
|
47705
|
-
throw new Error("token endpoint response did not include an access_token");
|
|
47706
|
-
}
|
|
47707
|
-
return {
|
|
47708
|
-
access_token: r2.access_token,
|
|
47709
|
-
...typeof r2.refresh_token === "string" ? { refresh_token: r2.refresh_token } : {},
|
|
47710
|
-
...typeof r2.expires_in === "number" ? { expires_in: r2.expires_in } : {},
|
|
47711
|
-
...typeof r2.scope === "string" ? { scope: r2.scope } : {}
|
|
47712
|
-
};
|
|
47713
|
-
}
|
|
47714
|
-
async function postToken(endpoint, body2, fetchImpl) {
|
|
47715
|
-
const res = await fetchImpl(endpoint, {
|
|
47716
|
-
method: "POST",
|
|
47717
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
47718
|
-
body: body2.toString()
|
|
47719
|
-
});
|
|
47720
|
-
if (!res.ok) {
|
|
47721
|
-
throw new Error(`token request failed at ${endpoint} (HTTP ${res.status})`);
|
|
47722
|
-
}
|
|
47723
|
-
return parseTokenResponse(await res.json());
|
|
47724
|
-
}
|
|
47725
|
-
async function exchangeCode(meta, opts, fetchImpl = fetch, now2 = Date.now) {
|
|
47726
|
-
const body2 = new URLSearchParams({
|
|
47727
|
-
grant_type: "authorization_code",
|
|
47728
|
-
client_id: opts.clientId,
|
|
47729
|
-
code: opts.code,
|
|
47730
|
-
code_verifier: opts.verifier,
|
|
47731
|
-
redirect_uri: opts.redirectUri,
|
|
47732
|
-
resource: opts.resource
|
|
47733
|
-
});
|
|
47734
|
-
if (opts.clientSecret) body2.set("client_secret", opts.clientSecret);
|
|
47735
|
-
const tokens = await postToken(meta.tokenEndpoint, body2, fetchImpl);
|
|
47736
|
-
return {
|
|
47737
|
-
kind: "oauth",
|
|
47738
|
-
accessToken: tokens.access_token,
|
|
47739
|
-
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {},
|
|
47740
|
-
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
47741
|
-
clientId: opts.clientId,
|
|
47742
|
-
...opts.clientSecret ? { clientSecret: opts.clientSecret } : {},
|
|
47743
|
-
authServer: opts.authServer,
|
|
47744
|
-
...tokens.scope ? { scope: tokens.scope } : {}
|
|
47745
|
-
};
|
|
47746
|
-
}
|
|
47747
|
-
async function refresh(meta, cred, fetchImpl = fetch, now2 = Date.now) {
|
|
47748
|
-
if (!cred.refreshToken) {
|
|
47749
|
-
throw new Error("credential has no refresh token to refresh with -- sign in again");
|
|
47750
|
-
}
|
|
47751
|
-
const body2 = new URLSearchParams({
|
|
47752
|
-
grant_type: "refresh_token",
|
|
47753
|
-
refresh_token: cred.refreshToken,
|
|
47754
|
-
client_id: cred.clientId
|
|
47755
|
-
});
|
|
47756
|
-
if (cred.clientSecret) body2.set("client_secret", cred.clientSecret);
|
|
47757
|
-
const tokens = await postToken(meta.tokenEndpoint, body2, fetchImpl);
|
|
47758
|
-
return {
|
|
47759
|
-
kind: "oauth",
|
|
47760
|
-
accessToken: tokens.access_token,
|
|
47761
|
-
// Keep the old refresh token when the server did not send a new one.
|
|
47762
|
-
refreshToken: tokens.refresh_token ?? cred.refreshToken,
|
|
47763
|
-
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
47764
|
-
clientId: cred.clientId,
|
|
47765
|
-
...cred.clientSecret ? { clientSecret: cred.clientSecret } : {},
|
|
47766
|
-
authServer: cred.authServer,
|
|
47767
|
-
scope: tokens.scope ?? cred.scope
|
|
47768
|
-
};
|
|
47769
|
-
}
|
|
47770
|
-
var EXPIRY_SKEW_MS = 6e4;
|
|
47771
|
-
function isExpired(cred, nowMs) {
|
|
47772
|
-
if (cred.expiresAt === void 0) return false;
|
|
47773
|
-
return cred.expiresAt - nowMs <= EXPIRY_SKEW_MS;
|
|
47774
|
-
}
|
|
47775
|
-
|
|
47776
|
-
// src/mcp/oauth/login.ts
|
|
48892
|
+
// ../mcp/src/oauth/login.ts
|
|
47777
48893
|
var defaultOpenBrowser = async (url) => {
|
|
47778
|
-
const { openBrowser: openBrowser3 } = await Promise.resolve().then(() => (
|
|
48894
|
+
const { openBrowser: openBrowser3 } = await Promise.resolve().then(() => (init_tools_node(), tools_node_exports));
|
|
47779
48895
|
await openBrowser3(url);
|
|
47780
48896
|
};
|
|
47781
48897
|
async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
47782
48898
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
47783
48899
|
const openBrowserFn = deps.openBrowser ?? defaultOpenBrowser;
|
|
47784
48900
|
const now2 = deps.now ?? Date.now;
|
|
48901
|
+
const redirectAbort = new AbortController();
|
|
47785
48902
|
const signInUrl = server.authUrl ?? server.url;
|
|
47786
48903
|
try {
|
|
47787
48904
|
const discovered = await discoverAuthServer(
|
|
@@ -47802,6 +48919,13 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
|
47802
48919
|
const meta = discovered.scopes ? { ...asMeta, scopesSupported: discovered.scopes } : asMeta;
|
|
47803
48920
|
const cached = await readMcpClient(stateDir, server.name);
|
|
47804
48921
|
const port = cached?.redirectPort ?? await getFreePort();
|
|
48922
|
+
if (cached?.redirectPort !== void 0 && !await portFree(cached.redirectPort)) {
|
|
48923
|
+
return {
|
|
48924
|
+
ok: false,
|
|
48925
|
+
reason: "failed",
|
|
48926
|
+
message: `the redirect port ${cached.redirectPort} is in use, and "${server.name}" is registered against exactly that port -- free it and sign in again`
|
|
48927
|
+
};
|
|
48928
|
+
}
|
|
47805
48929
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
47806
48930
|
const registered2 = cached && cached.authServer === discovered.authServer ? { clientId: cached.clientId, ...cached.clientSecret ? { clientSecret: cached.clientSecret } : {} } : await registerClient(
|
|
47807
48931
|
meta,
|
|
@@ -47812,7 +48936,25 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
|
47812
48936
|
await writeMcpClient(stateDir, server.name, {
|
|
47813
48937
|
clientId: registered2.clientId,
|
|
47814
48938
|
...registered2.clientSecret ? { clientSecret: registered2.clientSecret } : {},
|
|
47815
|
-
authServer: discovered.authServer
|
|
48939
|
+
authServer: discovered.authServer,
|
|
48940
|
+
/* PIN THE PORT WE JUST REGISTERED WITH.
|
|
48941
|
+
*
|
|
48942
|
+
* RFC 8252 s7.3 obliges an authorization server to accept any port on a
|
|
48943
|
+
* loopback redirect, and most do -- but not all, and the ones that do
|
|
48944
|
+
* not fail in the worst possible way. Atlassian's `auth.atlassian.com`
|
|
48945
|
+
* (Trello's authorization server, distinct from `mcp.atlassian.com`)
|
|
48946
|
+
* matches the redirect URI exactly. Registering with one ephemeral port
|
|
48947
|
+
* and then authorizing with a fresh one on the next sign-in produced
|
|
48948
|
+
* "The app's callback URL is invalid", from the vendor's own page,
|
|
48949
|
+
* BEFORE any token exchange -- so nothing came back for the
|
|
48950
|
+
* invalid_client recovery below to catch, and the flow simply timed out
|
|
48951
|
+
* waiting for a redirect that was never going to arrive.
|
|
48952
|
+
*
|
|
48953
|
+
* Persisting it costs nothing where the port is ignored, and is the
|
|
48954
|
+
* whole difference where it is not. The field already existed for
|
|
48955
|
+
* operator-set ports (`corenel mcp set-client --redirect-port`); it was
|
|
48956
|
+
* read here and never written by this path. */
|
|
48957
|
+
redirectPort: port
|
|
47816
48958
|
});
|
|
47817
48959
|
}
|
|
47818
48960
|
if (!registered2) {
|
|
@@ -47833,7 +48975,7 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
|
47833
48975
|
resource: discovered.resource
|
|
47834
48976
|
});
|
|
47835
48977
|
deps.onAuthUrl?.(authUrl);
|
|
47836
|
-
const redirectP = awaitRedirect(port, state);
|
|
48978
|
+
const redirectP = awaitRedirect(port, state, { signal: redirectAbort.signal });
|
|
47837
48979
|
redirectP.catch(() => {
|
|
47838
48980
|
});
|
|
47839
48981
|
await openBrowserFn(authUrl);
|
|
@@ -47855,6 +48997,7 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
|
47855
48997
|
await writeMcpCredential(stateDir, server.name, cred);
|
|
47856
48998
|
return { ok: true };
|
|
47857
48999
|
} catch (e2) {
|
|
49000
|
+
redirectAbort.abort();
|
|
47858
49001
|
const message = e2 instanceof Error ? e2.message : String(e2);
|
|
47859
49002
|
if (/invalid_client|unauthorized_client|client.{0,20}not.{0,10}found/i.test(message)) {
|
|
47860
49003
|
await clearMcpClient(stateDir, server.name).catch(() => {
|
|
@@ -48351,7 +49494,7 @@ function persistAddedRoots(controller, stateDir, log) {
|
|
|
48351
49494
|
import { spawn as spawn7 } from "node:child_process";
|
|
48352
49495
|
import { promises as fs16 } from "node:fs";
|
|
48353
49496
|
import { join as join20, resolve as resolve13 } from "node:path";
|
|
48354
|
-
var
|
|
49497
|
+
var DEFAULT_TIMEOUT_MS4 = 10 * 6e4;
|
|
48355
49498
|
var ALLOWED_SCHEMES = ["https://", "ssh://", "git+ssh://"];
|
|
48356
49499
|
var SCP_LIKE = /^[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+:[A-Za-z0-9_./~-]+$/;
|
|
48357
49500
|
function assertCloneUrl(url) {
|
|
@@ -48415,8 +49558,8 @@ function makeGitRunner(bin = "git") {
|
|
|
48415
49558
|
const timer = setTimeout(() => {
|
|
48416
49559
|
child.kill("SIGKILL");
|
|
48417
49560
|
settle({ code: -1, stdout, stderr: `${stderr}
|
|
48418
|
-
git timed out after ${opts?.timeoutMs ??
|
|
48419
|
-
}, opts?.timeoutMs ??
|
|
49561
|
+
git timed out after ${opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS4}ms` });
|
|
49562
|
+
}, opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS4);
|
|
48420
49563
|
timer.unref?.();
|
|
48421
49564
|
child.on("error", (e2) => {
|
|
48422
49565
|
clearTimeout(timer);
|
|
@@ -48525,7 +49668,7 @@ var RpcError = class extends Error {
|
|
|
48525
49668
|
if (shape.data !== void 0) this.data = shape.data;
|
|
48526
49669
|
}
|
|
48527
49670
|
};
|
|
48528
|
-
function
|
|
49671
|
+
function isRecord3(v2) {
|
|
48529
49672
|
return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
|
|
48530
49673
|
}
|
|
48531
49674
|
var JsonRpcPeer = class {
|
|
@@ -48563,7 +49706,7 @@ var JsonRpcPeer = class {
|
|
|
48563
49706
|
this.log(`acp: dropped unparseable frame (${line.length} bytes)`);
|
|
48564
49707
|
return;
|
|
48565
49708
|
}
|
|
48566
|
-
if (!
|
|
49709
|
+
if (!isRecord3(msg)) {
|
|
48567
49710
|
this.log("acp: dropped non-object frame");
|
|
48568
49711
|
return;
|
|
48569
49712
|
}
|
|
@@ -48585,7 +49728,7 @@ var JsonRpcPeer = class {
|
|
|
48585
49728
|
return;
|
|
48586
49729
|
}
|
|
48587
49730
|
this.pending.delete(id);
|
|
48588
|
-
if ("error" in msg &&
|
|
49731
|
+
if ("error" in msg && isRecord3(msg.error)) {
|
|
48589
49732
|
const e2 = msg.error;
|
|
48590
49733
|
p2.reject(new RpcError({
|
|
48591
49734
|
code: typeof e2.code === "number" ? e2.code : RPC_INTERNAL_ERROR,
|
|
@@ -49477,7 +50620,6 @@ function textOf(prompt) {
|
|
|
49477
50620
|
}
|
|
49478
50621
|
|
|
49479
50622
|
// src/acp/agentCli.ts
|
|
49480
|
-
init_store();
|
|
49481
50623
|
function elog(line) {
|
|
49482
50624
|
process.stderr.write(`${line}
|
|
49483
50625
|
`);
|
|
@@ -49525,434 +50667,239 @@ async function runAcpAgent(argv) {
|
|
|
49525
50667
|
});
|
|
49526
50668
|
}
|
|
49527
50669
|
|
|
49528
|
-
// src/
|
|
49529
|
-
|
|
49530
|
-
init_store();
|
|
49531
|
-
init_fileService();
|
|
49532
|
-
import { relative as relative7, resolve as resolvePath } from "node:path";
|
|
50670
|
+
// src/auth/login.ts
|
|
50671
|
+
import { createServer as createServer2 } from "node:http";
|
|
49533
50672
|
|
|
49534
|
-
// src/
|
|
49535
|
-
|
|
49536
|
-
|
|
49537
|
-
|
|
49538
|
-
var McpHttpClient = class {
|
|
49539
|
-
constructor(server, auth, fetchImpl = fetch) {
|
|
49540
|
-
this.server = server;
|
|
49541
|
-
this.auth = auth;
|
|
49542
|
-
this.fetchImpl = fetchImpl;
|
|
49543
|
-
}
|
|
49544
|
-
nextRequestId = 1;
|
|
49545
|
-
sessionId;
|
|
49546
|
-
async initialize() {
|
|
49547
|
-
await this.send("initialize", {
|
|
49548
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
49549
|
-
capabilities: {},
|
|
49550
|
-
clientInfo: CLIENT_INFO
|
|
49551
|
-
});
|
|
49552
|
-
}
|
|
49553
|
-
async listTools() {
|
|
49554
|
-
const result = await this.send("tools/list", {});
|
|
49555
|
-
const tools = isRecord3(result) ? result.tools : void 0;
|
|
49556
|
-
if (!Array.isArray(tools)) return [];
|
|
49557
|
-
return tools.filter(isMcpTool);
|
|
49558
|
-
}
|
|
49559
|
-
async callTool(name, args) {
|
|
49560
|
-
const result = await this.send("tools/call", { name, arguments: args });
|
|
49561
|
-
const record = isRecord3(result) ? result : {};
|
|
49562
|
-
return { content: record.content, isError: record.isError === true };
|
|
49563
|
-
}
|
|
49564
|
-
/** Send one JSON-RPC request, retrying exactly once on a 401 that the
|
|
49565
|
-
* AuthProvider says it recovered from. `isRetry` prevents a second retry
|
|
49566
|
-
* loop -- a credential that fails twice is refused, not flaky. */
|
|
49567
|
-
async send(method, params, isRetry = false) {
|
|
49568
|
-
const headers = await this.buildHeaders();
|
|
49569
|
-
const id = this.nextRequestId++;
|
|
49570
|
-
const res = await this.request(headers, id, method, params);
|
|
49571
|
-
if (res.status === 401) {
|
|
49572
|
-
if (!isRetry && await this.auth.onUnauthorized()) {
|
|
49573
|
-
return this.send(method, params, true);
|
|
49574
|
-
}
|
|
49575
|
-
throw new Error(
|
|
49576
|
-
`MCP server "${this.server.name}" refused the credential (401) -- run: corenel mcp login ${this.server.name}`
|
|
49577
|
-
);
|
|
49578
|
-
}
|
|
49579
|
-
const sessionId = res.headers.get(SESSION_HEADER);
|
|
49580
|
-
if (sessionId) this.sessionId = sessionId;
|
|
49581
|
-
if (!res.ok) {
|
|
49582
|
-
throw new Error(`MCP server "${this.server.name}" returned HTTP ${res.status}`);
|
|
49583
|
-
}
|
|
49584
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
49585
|
-
let body2;
|
|
49586
|
-
if (contentType.includes("text/event-stream")) {
|
|
49587
|
-
body2 = await this.readSseResponse(res, id);
|
|
49588
|
-
} else if (contentType.includes("application/json")) {
|
|
49589
|
-
body2 = await res.json();
|
|
49590
|
-
} else {
|
|
49591
|
-
throw new Error(
|
|
49592
|
-
`MCP server "${this.server.name}" answered with an unexpected content-type: "${contentType || "(none)"}"`
|
|
49593
|
-
);
|
|
49594
|
-
}
|
|
49595
|
-
if (body2.error) {
|
|
49596
|
-
throw new Error(`MCP server "${this.server.name}" returned error ${body2.error.code}: ${body2.error.message}`);
|
|
49597
|
-
}
|
|
49598
|
-
return body2.result;
|
|
49599
|
-
}
|
|
49600
|
-
/** Read an SSE body until the JSON-RPC response for `id` arrives.
|
|
49601
|
-
*
|
|
49602
|
-
* Events are separated by a blank line; an event's `data:` lines are joined
|
|
49603
|
-
* with newlines, per the SSE spec, and parsed as one JSON-RPC message.
|
|
49604
|
-
* Notifications (no `id`) and responses to other ids are skipped. The reader
|
|
49605
|
-
* is cancelled once the answer is in, so a server that keeps the stream open
|
|
49606
|
-
* after replying cannot hold this call open with it. */
|
|
49607
|
-
async readSseResponse(res, id) {
|
|
49608
|
-
if (!res.body) {
|
|
49609
|
-
throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
|
|
49610
|
-
}
|
|
49611
|
-
const reader = res.body.getReader();
|
|
49612
|
-
const decoder = new TextDecoder();
|
|
49613
|
-
let buffer = "";
|
|
49614
|
-
try {
|
|
49615
|
-
for (; ; ) {
|
|
49616
|
-
const { done, value } = await reader.read();
|
|
49617
|
-
if (value) buffer += decoder.decode(value, { stream: true });
|
|
49618
|
-
buffer = buffer.replace(/\r\n/g, "\n");
|
|
49619
|
-
let boundary = buffer.indexOf("\n\n");
|
|
49620
|
-
while (boundary !== -1) {
|
|
49621
|
-
const event = buffer.slice(0, boundary);
|
|
49622
|
-
buffer = buffer.slice(boundary + 2);
|
|
49623
|
-
const match = this.matchSseEvent(event, id);
|
|
49624
|
-
if (match) return match;
|
|
49625
|
-
boundary = buffer.indexOf("\n\n");
|
|
49626
|
-
}
|
|
49627
|
-
if (done) {
|
|
49628
|
-
const tail3 = buffer.trim() ? this.matchSseEvent(buffer, id) : null;
|
|
49629
|
-
if (tail3) return tail3;
|
|
49630
|
-
throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
|
|
49631
|
-
}
|
|
49632
|
-
}
|
|
49633
|
-
} finally {
|
|
49634
|
-
reader.cancel().catch(() => {
|
|
49635
|
-
});
|
|
49636
|
-
}
|
|
49637
|
-
}
|
|
49638
|
-
/** The JSON-RPC response in one SSE event, if it answers `id`; else null. */
|
|
49639
|
-
matchSseEvent(event, id) {
|
|
49640
|
-
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
49641
|
-
if (!data) return null;
|
|
49642
|
-
let parsed;
|
|
49643
|
-
try {
|
|
49644
|
-
parsed = JSON.parse(data);
|
|
49645
|
-
} catch {
|
|
49646
|
-
return null;
|
|
49647
|
-
}
|
|
49648
|
-
if (!isRecord3(parsed) || parsed.id !== id) return null;
|
|
49649
|
-
if (!("result" in parsed) && !("error" in parsed)) return null;
|
|
49650
|
-
return parsed;
|
|
49651
|
-
}
|
|
49652
|
-
/** The bare `fetch()` call, wrapped -- every OTHER throw in `send` already
|
|
49653
|
-
* names the server; this is the one call whose own failures (DNS miss,
|
|
49654
|
-
* connection refused, TLS failure) never reach `send`'s response-handling
|
|
49655
|
-
* at all. Left unguarded, node's bare `fetch failed` -- naming neither the
|
|
49656
|
-
* URL nor the actual cause -- propagates straight out of `send`, into
|
|
49657
|
-
* `registry.ts`'s `status().error`, and out of `doctor`, the one command
|
|
49658
|
-
* whose entire job is diagnosis. `err.cause` carries the real reason
|
|
49659
|
-
* (ECONNREFUSED, ENOTFOUND, a TLS alert); undici puts it there rather than
|
|
49660
|
-
* in `err.message`. */
|
|
49661
|
-
async request(headers, id, method, params) {
|
|
49662
|
-
try {
|
|
49663
|
-
return await this.fetchImpl(this.server.url, {
|
|
49664
|
-
method: "POST",
|
|
49665
|
-
headers,
|
|
49666
|
-
body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
49667
|
-
});
|
|
49668
|
-
} catch (err) {
|
|
49669
|
-
const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : err instanceof Error ? err.message : String(err);
|
|
49670
|
-
throw new Error(`MCP server "${this.server.name}" (${this.server.url}) is unreachable: ${cause}`);
|
|
49671
|
-
}
|
|
49672
|
-
}
|
|
49673
|
-
async buildHeaders() {
|
|
49674
|
-
const headers = new Headers();
|
|
49675
|
-
headers.set("Content-Type", "application/json");
|
|
49676
|
-
headers.set("Accept", "application/json, text/event-stream");
|
|
49677
|
-
for (const h2 of await this.auth.headers()) headers.set(h2.name, h2.value);
|
|
49678
|
-
if (this.sessionId) headers.set(SESSION_HEADER, this.sessionId);
|
|
49679
|
-
return headers;
|
|
49680
|
-
}
|
|
49681
|
-
};
|
|
49682
|
-
function isRecord3(value) {
|
|
49683
|
-
return typeof value === "object" && value !== null;
|
|
50673
|
+
// src/auth/pkce.ts
|
|
50674
|
+
import { createHash as createHash4, randomBytes as randomBytes6 } from "node:crypto";
|
|
50675
|
+
function base64url2(buf) {
|
|
50676
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
49684
50677
|
}
|
|
49685
|
-
function
|
|
49686
|
-
return
|
|
50678
|
+
function createVerifier(bytes = 32) {
|
|
50679
|
+
return base64url2(randomBytes6(bytes));
|
|
50680
|
+
}
|
|
50681
|
+
function challengeFor(verifier) {
|
|
50682
|
+
return base64url2(createHash4("sha256").update(verifier).digest());
|
|
50683
|
+
}
|
|
50684
|
+
function createState() {
|
|
50685
|
+
return base64url2(randomBytes6(16));
|
|
50686
|
+
}
|
|
50687
|
+
function createPkce() {
|
|
50688
|
+
const verifier = createVerifier();
|
|
50689
|
+
return { verifier, challenge: challengeFor(verifier), method: "S256", state: createState() };
|
|
50690
|
+
}
|
|
50691
|
+
function authorizeUrl(p2) {
|
|
50692
|
+
const u2 = new URL("/oauth/authorize", p2.issuer);
|
|
50693
|
+
u2.searchParams.set("response_type", "code");
|
|
50694
|
+
u2.searchParams.set("client_id", p2.clientId);
|
|
50695
|
+
u2.searchParams.set("redirect_uri", p2.redirectUri);
|
|
50696
|
+
u2.searchParams.set("code_challenge", p2.pkce.challenge);
|
|
50697
|
+
u2.searchParams.set("code_challenge_method", p2.pkce.method);
|
|
50698
|
+
u2.searchParams.set("state", p2.pkce.state);
|
|
50699
|
+
u2.searchParams.set("scope", p2.scope ?? "profile email");
|
|
50700
|
+
return u2.toString();
|
|
50701
|
+
}
|
|
50702
|
+
function stateMatches(expected, got) {
|
|
50703
|
+
if (!got || got.length !== expected.length) return false;
|
|
50704
|
+
let diff = 0;
|
|
50705
|
+
for (let i2 = 0; i2 < expected.length; i2++) diff |= expected.charCodeAt(i2) ^ got.charCodeAt(i2);
|
|
50706
|
+
return diff === 0;
|
|
49687
50707
|
}
|
|
49688
50708
|
|
|
49689
|
-
// src/
|
|
49690
|
-
|
|
49691
|
-
|
|
49692
|
-
|
|
49693
|
-
|
|
50709
|
+
// src/auth/login.ts
|
|
50710
|
+
var DEFAULT_TIMEOUT_MS5 = 5 * 60 * 1e3;
|
|
50711
|
+
var PAGE_OK2 = `<!doctype html><meta charset="utf-8"><title>Signed in</title>
|
|
50712
|
+
<body style="font:16px system-ui;padding:3rem;text-align:center">
|
|
50713
|
+
<h1>Signed in to Corenel</h1><p>You can close this tab and return to your terminal.</p></body>`;
|
|
50714
|
+
var PAGE_FAIL2 = `<!doctype html><meta charset="utf-8"><title>Sign-in failed</title>
|
|
50715
|
+
<body style="font:16px system-ui;padding:3rem;text-align:center">
|
|
50716
|
+
<h1>Sign-in failed</h1><p>Return to your terminal for details.</p></body>`;
|
|
50717
|
+
function awaitCallback(pkce, port, timeoutMs) {
|
|
50718
|
+
let resolveUri;
|
|
50719
|
+
let resolveResult;
|
|
50720
|
+
let rejectResult;
|
|
50721
|
+
const redirectUri = new Promise((res) => {
|
|
50722
|
+
resolveUri = res;
|
|
50723
|
+
});
|
|
50724
|
+
const result = new Promise((res, rej) => {
|
|
50725
|
+
resolveResult = res;
|
|
50726
|
+
rejectResult = rej;
|
|
50727
|
+
});
|
|
50728
|
+
const server = createServer2((req, res) => {
|
|
50729
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
50730
|
+
if (url.pathname !== "/callback") {
|
|
50731
|
+
res.writeHead(404).end();
|
|
50732
|
+
return;
|
|
49694
50733
|
}
|
|
49695
|
-
const
|
|
49696
|
-
const
|
|
49697
|
-
|
|
49698
|
-
|
|
49699
|
-
|
|
49700
|
-
|
|
49701
|
-
|
|
49702
|
-
const cred = await readMcpCredential(stateDir, server.name);
|
|
49703
|
-
if (!cred && !server.hosted) {
|
|
49704
|
-
throw new Error(`No credential stored for MCP server "${server.name}" -- run: corenel mcp login ${server.name} (or set-header, for a static token).`);
|
|
49705
|
-
}
|
|
49706
|
-
const mcpHeaders = !cred ? [] : cred.kind === "headers" ? cred.headers : [{ name: "Authorization", value: `Bearer ${(isExpired(cred, Date.now()) ? await doRefresh(cred) : cred).accessToken}` }];
|
|
49707
|
-
if (!server.hosted) return mcpHeaders;
|
|
49708
|
-
const moved = mcpHeaders.map((h2) => h2.name.toLowerCase() === "authorization" ? { name: "X-MCP-Authorization", value: h2.value } : h2);
|
|
49709
|
-
const account = await accountToken();
|
|
49710
|
-
return account ? [...moved, { name: "Authorization", value: `Bearer ${account}` }] : moved;
|
|
49711
|
-
},
|
|
49712
|
-
async onUnauthorized() {
|
|
49713
|
-
const cred = await readMcpCredential(stateDir, server.name);
|
|
49714
|
-
if (!cred || cred.kind !== "oauth") return false;
|
|
49715
|
-
try {
|
|
49716
|
-
await doRefresh(cred);
|
|
49717
|
-
return true;
|
|
49718
|
-
} catch {
|
|
49719
|
-
return false;
|
|
49720
|
-
}
|
|
50734
|
+
const err = url.searchParams.get("error");
|
|
50735
|
+
const code2 = url.searchParams.get("code");
|
|
50736
|
+
const state = url.searchParams.get("state");
|
|
50737
|
+
if (!stateMatches(pkce.state, state)) {
|
|
50738
|
+
res.writeHead(400, { "content-type": "text/html" }).end(PAGE_FAIL2);
|
|
50739
|
+
rejectResult(new Error("state mismatch \u2014 the callback did not match this sign-in request"));
|
|
50740
|
+
return;
|
|
49721
50741
|
}
|
|
49722
|
-
|
|
49723
|
-
}
|
|
49724
|
-
|
|
49725
|
-
|
|
49726
|
-
init_store();
|
|
49727
|
-
|
|
49728
|
-
// src/mcp/timedFetch.ts
|
|
49729
|
-
var AUTH_REQUEST_TIMEOUT_MS = 15e3;
|
|
49730
|
-
function timedFetch(ms, inner = fetch) {
|
|
49731
|
-
return async (input, init2 = {}) => {
|
|
49732
|
-
const deadline = AbortSignal.timeout(ms);
|
|
49733
|
-
const signal = init2.signal ? AbortSignal.any([init2.signal, deadline]) : deadline;
|
|
49734
|
-
try {
|
|
49735
|
-
return await inner(input, { ...init2, signal });
|
|
49736
|
-
} catch (err) {
|
|
49737
|
-
if (deadline.aborted && !init2.signal?.aborted) {
|
|
49738
|
-
throw new Error(`no answer within ${ms / 1e3}s`, { cause: err });
|
|
49739
|
-
}
|
|
49740
|
-
throw err;
|
|
50742
|
+
if (err) {
|
|
50743
|
+
res.writeHead(400, { "content-type": "text/html" }).end(PAGE_FAIL2);
|
|
50744
|
+
rejectResult(new Error(`authorization failed: ${err}`));
|
|
50745
|
+
return;
|
|
49741
50746
|
}
|
|
50747
|
+
if (!code2) {
|
|
50748
|
+
res.writeHead(400, { "content-type": "text/html" }).end(PAGE_FAIL2);
|
|
50749
|
+
rejectResult(new Error("callback carried no authorization code"));
|
|
50750
|
+
return;
|
|
50751
|
+
}
|
|
50752
|
+
res.writeHead(200, { "content-type": "text/html" }).end(PAGE_OK2);
|
|
50753
|
+
resolveResult({ code: code2 });
|
|
50754
|
+
});
|
|
50755
|
+
const timer = setTimeout(() => {
|
|
50756
|
+
rejectResult(new Error("timed out waiting for the browser callback"));
|
|
50757
|
+
}, timeoutMs);
|
|
50758
|
+
timer.unref?.();
|
|
50759
|
+
const close2 = () => {
|
|
50760
|
+
clearTimeout(timer);
|
|
50761
|
+
server.close();
|
|
49742
50762
|
};
|
|
50763
|
+
void result.finally(close2).catch(() => {
|
|
50764
|
+
});
|
|
50765
|
+
server.listen(port, "127.0.0.1", () => {
|
|
50766
|
+
const addr = server.address();
|
|
50767
|
+
const bound = typeof addr === "object" && addr ? addr.port : port;
|
|
50768
|
+
resolveUri(`http://127.0.0.1:${bound}/callback`);
|
|
50769
|
+
});
|
|
50770
|
+
return { redirectUri, result, close: close2 };
|
|
49743
50771
|
}
|
|
49744
|
-
|
|
49745
|
-
|
|
49746
|
-
|
|
49747
|
-
|
|
49748
|
-
|
|
49749
|
-
|
|
49750
|
-
|
|
49751
|
-
|
|
49752
|
-
}
|
|
49753
|
-
}
|
|
49754
|
-
function defaultClientFactory(stateDir, server) {
|
|
49755
|
-
return new McpHttpClient(
|
|
49756
|
-
server,
|
|
49757
|
-
authProviderFor(stateDir, server, timedFetch(AUTH_REQUEST_TIMEOUT_MS), () => accountTokenFor(stateDir))
|
|
50772
|
+
async function login(deps) {
|
|
50773
|
+
const log = deps.log ?? (() => void 0);
|
|
50774
|
+
const now2 = deps.now ?? Date.now;
|
|
50775
|
+
const pkce = createPkce();
|
|
50776
|
+
const { redirectUri: uriP, result, close: close2 } = awaitCallback(
|
|
50777
|
+
pkce,
|
|
50778
|
+
deps.config.port ?? 0,
|
|
50779
|
+
deps.timeoutMs ?? DEFAULT_TIMEOUT_MS5
|
|
49758
50780
|
);
|
|
49759
|
-
|
|
49760
|
-
|
|
49761
|
-
|
|
49762
|
-
|
|
49763
|
-
|
|
49764
|
-
|
|
49765
|
-
|
|
49766
|
-
|
|
49767
|
-
statuses = /* @__PURE__ */ new Map();
|
|
49768
|
-
// The order the operator wrote servers in daemon.json, captured once per
|
|
49769
|
-
// connectAll() -- `tools()` walks THIS, never `connected`'s Map iteration
|
|
49770
|
-
// order. `connectOne` runs concurrently for every server (Promise.all), so
|
|
49771
|
-
// Map insertion order is race order: whichever server's initialize+list
|
|
49772
|
-
// happened to resolve first. That is fine for `connected` (a lookup table,
|
|
49773
|
-
// order never read) but `tools()` is a list a model/doctor/policy consumes
|
|
49774
|
-
// in order, and race order means a different tool ordering on every daemon
|
|
49775
|
-
// boot -- exactly the kind of bug that surfaces far from here and is never
|
|
49776
|
-
// traced back. Configured order is also information the operator supplied
|
|
49777
|
-
// deliberately; sorting alphabetically would discard it.
|
|
49778
|
-
serverOrder = [];
|
|
49779
|
-
/**
|
|
49780
|
-
* Connect to every configured server and cache its tools. Never throws --
|
|
49781
|
-
* a server that fails to initialize, or fails listing tools, is recorded
|
|
49782
|
-
* in `status()` with zero tools and the daemon keeps starting.
|
|
49783
|
-
*/
|
|
49784
|
-
async connectAll(opts = {}) {
|
|
49785
|
-
this.connected.clear();
|
|
49786
|
-
this.statuses.clear();
|
|
49787
|
-
const servers = await readMcpServers(this.stateDir);
|
|
49788
|
-
this.serverOrder = servers.map((s2) => s2.name);
|
|
49789
|
-
const timeoutMs = opts.timeoutMs ?? SERVER_CONNECT_TIMEOUT_MS;
|
|
49790
|
-
await Promise.all(servers.map((server) => this.connectOne(server, timeoutMs, opts.onServer)));
|
|
49791
|
-
}
|
|
49792
|
-
/* BOUNDED, AND SETTLED EXACTLY ONCE.
|
|
49793
|
-
*
|
|
49794
|
-
* Every server connects under one Promise.all, and daemon startup awaits it.
|
|
49795
|
-
* With no bound, one stalled server -- measured: an expired Microsoft 365
|
|
49796
|
-
* token refreshing against a cold service, still waiting at 60 seconds --
|
|
49797
|
-
* left the terminal silent after `mirrors sessions`, indistinguishable from a
|
|
49798
|
-
* crash.
|
|
49799
|
-
*
|
|
49800
|
-
* The deadline covers the whole connect (any credential refresh, initialize,
|
|
49801
|
-
* tools/list), not each request, because several individually bounded steps
|
|
49802
|
-
* in a row can still add up to a hang. The attempt that loses the race keeps
|
|
49803
|
-
* running; its late result is deliberately ignored, so a server reported as
|
|
49804
|
-
* timed out cannot quietly become connected afterwards -- status would then
|
|
49805
|
-
* tell doctor one thing and the registered tools another. */
|
|
49806
|
-
async connectOne(server, timeoutMs, onServer) {
|
|
49807
|
-
let timer;
|
|
49808
|
-
const deadline = new Promise((resolve14) => {
|
|
49809
|
-
timer = setTimeout(() => resolve14("timeout"), timeoutMs);
|
|
50781
|
+
try {
|
|
50782
|
+
const redirectUri = await uriP;
|
|
50783
|
+
const url = authorizeUrl({
|
|
50784
|
+
issuer: deps.config.issuer,
|
|
50785
|
+
clientId: deps.config.clientId,
|
|
50786
|
+
redirectUri,
|
|
50787
|
+
pkce,
|
|
50788
|
+
...deps.config.scope ? { scope: deps.config.scope } : {}
|
|
49810
50789
|
});
|
|
49811
|
-
|
|
49812
|
-
|
|
49813
|
-
|
|
49814
|
-
|
|
49815
|
-
|
|
49816
|
-
|
|
49817
|
-
|
|
50790
|
+
log("Opening your browser to sign in...");
|
|
50791
|
+
log(url);
|
|
50792
|
+
await deps.openBrowser(url);
|
|
50793
|
+
const { code: code2 } = await result;
|
|
50794
|
+
const tokens = await deps.exchange({
|
|
50795
|
+
issuer: deps.config.issuer,
|
|
50796
|
+
clientId: deps.config.clientId,
|
|
50797
|
+
code: code2,
|
|
50798
|
+
verifier: pkce.verifier,
|
|
50799
|
+
redirectUri
|
|
49818
50800
|
});
|
|
49819
|
-
|
|
49820
|
-
|
|
49821
|
-
|
|
49822
|
-
|
|
49823
|
-
}
|
|
49824
|
-
|
|
49825
|
-
|
|
49826
|
-
|
|
49827
|
-
|
|
49828
|
-
|
|
49829
|
-
|
|
49830
|
-
|
|
49831
|
-
|
|
49832
|
-
|
|
49833
|
-
|
|
49834
|
-
clearTimeout(timer);
|
|
49835
|
-
}
|
|
49836
|
-
const settled = this.statuses.get(server.name);
|
|
49837
|
-
if (settled) onServer?.({ server: server.name, ...settled });
|
|
49838
|
-
}
|
|
49839
|
-
/** Every tool from every connected server, paired with the server it came
|
|
49840
|
-
* from, in CONFIGURED order (`serverOrder`) -- not the order servers
|
|
49841
|
-
* happened to finish connecting in. Within a server, tools stay in the
|
|
49842
|
-
* order that server returned them. A server with no entry in `connected`
|
|
49843
|
-
* either failed to connect or reported zero tools -- `status()` tells the
|
|
49844
|
-
* two apart. */
|
|
49845
|
-
tools() {
|
|
49846
|
-
const out = [];
|
|
49847
|
-
for (const server of this.serverOrder) {
|
|
49848
|
-
const entry = this.connected.get(server);
|
|
49849
|
-
if (!entry) continue;
|
|
49850
|
-
for (const tool of entry.tools) out.push({ server, tool });
|
|
49851
|
-
}
|
|
49852
|
-
return out;
|
|
49853
|
-
}
|
|
49854
|
-
/**
|
|
49855
|
-
* Call one tool on one server. Fails immediately, naming the server, when
|
|
49856
|
-
* that server never connected -- there is nothing to route the call
|
|
49857
|
-
* through, and pretending otherwise would either stall waiting on a
|
|
49858
|
-
* connection that will never appear or return an empty result the caller
|
|
49859
|
-
* could mistake for a genuine (if empty) tool response.
|
|
49860
|
-
*/
|
|
49861
|
-
async call(server, tool, args) {
|
|
49862
|
-
const entry = this.connected.get(server);
|
|
49863
|
-
if (!entry) {
|
|
49864
|
-
throw new Error(`MCP server "${server}" is not connected -- run: corenel mcp doctor`);
|
|
49865
|
-
}
|
|
49866
|
-
return entry.client.callTool(tool, args);
|
|
49867
|
-
}
|
|
49868
|
-
/** Per-server connection state, for `doctor` (Task 10) to print. A server
|
|
49869
|
-
* with `connected: false` always carries `error` -- there is no code path
|
|
49870
|
-
* that records a failure without a reason, because "no tools" alone tells
|
|
49871
|
-
* the reader nothing about what to fix. */
|
|
49872
|
-
status() {
|
|
49873
|
-
const out = [];
|
|
49874
|
-
for (const server of this.serverOrder) {
|
|
49875
|
-
const s2 = this.statuses.get(server);
|
|
49876
|
-
if (s2) out.push({ server, ...s2 });
|
|
49877
|
-
}
|
|
49878
|
-
return out;
|
|
50801
|
+
if (!tokens.access_token) throw new Error("the token exchange returned no access token");
|
|
50802
|
+
const cred = {
|
|
50803
|
+
accessToken: tokens.access_token,
|
|
50804
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {},
|
|
50805
|
+
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
50806
|
+
...tokens.account ? { account: tokens.account } : {},
|
|
50807
|
+
...deps.apiBase ? { apiBase: deps.apiBase } : {}
|
|
50808
|
+
};
|
|
50809
|
+
await saveCredential(cred, deps.stateDir);
|
|
50810
|
+
return {
|
|
50811
|
+
...cred.account ? { account: cred.account } : {},
|
|
50812
|
+
...cred.expiresAt ? { expiresAt: cred.expiresAt } : {}
|
|
50813
|
+
};
|
|
50814
|
+
} finally {
|
|
50815
|
+
close2();
|
|
49879
50816
|
}
|
|
49880
|
-
}
|
|
49881
|
-
|
|
49882
|
-
// src/mcp/toolNames.ts
|
|
49883
|
-
init_protocol();
|
|
50817
|
+
}
|
|
49884
50818
|
|
|
49885
|
-
// src/
|
|
49886
|
-
|
|
49887
|
-
|
|
50819
|
+
// src/auth/cli.ts
|
|
50820
|
+
init_tools_node();
|
|
50821
|
+
function oauthConfigFromEnv(env = process.env) {
|
|
50822
|
+
const issuer = env.CORENEL_OAUTH_ISSUER?.trim();
|
|
50823
|
+
const clientId = env.CORENEL_OAUTH_CLIENT_ID?.trim();
|
|
50824
|
+
if (!issuer || !clientId) return null;
|
|
50825
|
+
return { issuer, clientId };
|
|
49888
50826
|
}
|
|
49889
|
-
function
|
|
49890
|
-
|
|
49891
|
-
|
|
50827
|
+
async function exchangeCode2(p2) {
|
|
50828
|
+
const res = await fetch(new URL("/oauth/token", p2.issuer), {
|
|
50829
|
+
method: "POST",
|
|
50830
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
50831
|
+
body: new URLSearchParams({
|
|
50832
|
+
grant_type: "authorization_code",
|
|
50833
|
+
client_id: p2.clientId,
|
|
50834
|
+
code: p2.code,
|
|
50835
|
+
code_verifier: p2.verifier,
|
|
50836
|
+
redirect_uri: p2.redirectUri
|
|
50837
|
+
}).toString()
|
|
50838
|
+
});
|
|
50839
|
+
if (!res.ok) {
|
|
50840
|
+
throw new Error(`token exchange failed (HTTP ${res.status})`);
|
|
50841
|
+
}
|
|
50842
|
+
return await res.json();
|
|
49892
50843
|
}
|
|
49893
|
-
function
|
|
49894
|
-
|
|
49895
|
-
if (
|
|
49896
|
-
|
|
49897
|
-
|
|
49898
|
-
|
|
50844
|
+
async function runLogin(stateDir, apiBase2) {
|
|
50845
|
+
const config = oauthConfigFromEnv();
|
|
50846
|
+
if (!config) {
|
|
50847
|
+
console.error("Sign-in is not configured on this build.");
|
|
50848
|
+
console.error("Set CORENEL_OAUTH_ISSUER and CORENEL_OAUTH_CLIENT_ID, or use " + TOKEN_ENV2 + " for headless use.");
|
|
50849
|
+
return 1;
|
|
50850
|
+
}
|
|
50851
|
+
try {
|
|
50852
|
+
const res = await login({
|
|
50853
|
+
config,
|
|
50854
|
+
openBrowser,
|
|
50855
|
+
exchange: exchangeCode2,
|
|
50856
|
+
log: (l2) => console.error(l2),
|
|
50857
|
+
...stateDir ? { stateDir } : {},
|
|
50858
|
+
// Recorded so the daemon does not have to guess later which API this
|
|
50859
|
+
// credential belongs to -- see LoginDeps.apiBase.
|
|
50860
|
+
...apiBase2 ? { apiBase: apiBase2 } : {}
|
|
49899
50861
|
});
|
|
49900
|
-
|
|
50862
|
+
console.error(`Signed in${res.account ? ` as ${res.account}` : ""}.`);
|
|
50863
|
+
return 0;
|
|
50864
|
+
} catch (err) {
|
|
50865
|
+
console.error(`Sign-in failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
50866
|
+
return 1;
|
|
49901
50867
|
}
|
|
49902
|
-
if (content == null) return "";
|
|
49903
|
-
return JSON.stringify(content);
|
|
49904
50868
|
}
|
|
49905
|
-
function
|
|
49906
|
-
const
|
|
49907
|
-
|
|
49908
|
-
|
|
49909
|
-
|
|
49910
|
-
|
|
49911
|
-
|
|
49912
|
-
|
|
49913
|
-
}
|
|
49914
|
-
out.push(toAgentTool(registry2, server, tool));
|
|
50869
|
+
async function runLogout(stateDir, env = process.env) {
|
|
50870
|
+
const before = await resolveCredential(env, stateDir);
|
|
50871
|
+
await clearCredential(stateDir);
|
|
50872
|
+
console.error("Signed out. (Paired devices are unaffected - run `hosts forget` to unpair.)");
|
|
50873
|
+
console.error(` Cleared ${tokenFilePath(stateDir)} and ${credentialFilePath(stateDir)}`);
|
|
50874
|
+
if (before.source === "none") console.error(" Nothing was stored - this machine was already signed out.");
|
|
50875
|
+
if (env[TOKEN_ENV2]?.trim()) {
|
|
50876
|
+
console.error(` NOTE: ${TOKEN_ENV2} is still set in this environment, so this machine is STILL signed in.`);
|
|
49915
50877
|
}
|
|
49916
|
-
return
|
|
50878
|
+
return 0;
|
|
49917
50879
|
}
|
|
49918
|
-
function
|
|
49919
|
-
|
|
49920
|
-
const address = mcpToolAddress(server, tool.name);
|
|
49921
|
-
return {
|
|
49922
|
-
name,
|
|
49923
|
-
namespace: "mcp",
|
|
49924
|
-
description: tool.description?.trim() || `MCP tool "${tool.name}" on server "${server}" (${address}).`,
|
|
49925
|
-
parameters: toParameters(tool.inputSchema),
|
|
49926
|
-
mutates: true,
|
|
49927
|
-
async run(args) {
|
|
49928
|
-
const result = await registry2.call(server, tool.name, args);
|
|
49929
|
-
const text = formatContent(result.content);
|
|
49930
|
-
return result.isError ? `Error: ${text}` : text;
|
|
49931
|
-
}
|
|
49932
|
-
};
|
|
50880
|
+
function sourceLabel(source) {
|
|
50881
|
+
return source === "env" ? `${TOKEN_ENV2} environment variable` : source === "token-file" ? "token file" : source === "credential-file" ? "credential file (auth.json)" : "none";
|
|
49933
50882
|
}
|
|
49934
|
-
|
|
49935
|
-
|
|
49936
|
-
|
|
49937
|
-
|
|
49938
|
-
|
|
49939
|
-
|
|
49940
|
-
|
|
49941
|
-
const service = new PermissionService({
|
|
49942
|
-
policy,
|
|
49943
|
-
defaultForReadOnly: "allow",
|
|
49944
|
-
defaultForMutating: "allow"
|
|
49945
|
-
});
|
|
49946
|
-
const level = service.resolveLevel({ name: address, address, mutates: true });
|
|
49947
|
-
if (level !== "allow") {
|
|
49948
|
-
return {
|
|
49949
|
-
ok: false,
|
|
49950
|
-
reason: `${address} resolves to permission "${level}" - MCP tools never prompt; set it to "allow" or remove the rule`
|
|
49951
|
-
};
|
|
50883
|
+
async function runAuthStatus(stateDir, env = process.env) {
|
|
50884
|
+
const r2 = await resolveCredential(env, stateDir);
|
|
50885
|
+
if (!r2.token) {
|
|
50886
|
+
console.error("Not signed in. Run `corenel login`, or set " + TOKEN_ENV2 + ".");
|
|
50887
|
+
console.error(` Looked in ${tokenFilePath(stateDir)}`);
|
|
50888
|
+
console.error(` Looked in ${credentialFilePath(stateDir)}`);
|
|
50889
|
+
return 1;
|
|
49952
50890
|
}
|
|
49953
|
-
|
|
50891
|
+
console.error(`Signed in (${r2.source === "env" ? sourceLabel(r2.source) : r2.path}).`);
|
|
50892
|
+
console.error(` account ${r2.account ?? "not recorded in this store - run `corenel login` to record it"}`);
|
|
50893
|
+
console.error(` token ${maskToken(r2.token)}`);
|
|
50894
|
+
console.error(` source ${sourceLabel(r2.source)}`);
|
|
50895
|
+
console.error(` expiry ${r2.expired ? `EXPIRED at ${new Date(r2.expiresAt ?? 0).toISOString()} - run login again` : r2.expiresAt ? new Date(r2.expiresAt).toISOString() : "not recorded in this store"}`);
|
|
50896
|
+
return r2.expired ? 1 : 0;
|
|
49954
50897
|
}
|
|
49955
50898
|
|
|
50899
|
+
// src/cli.ts
|
|
50900
|
+
init_fileService();
|
|
50901
|
+
import { relative as relative7, resolve as resolvePath } from "node:path";
|
|
50902
|
+
|
|
49956
50903
|
// src/mcp/daemonTools.ts
|
|
49957
50904
|
function buildMcpPolicy(fileConfig) {
|
|
49958
50905
|
return {
|
|
@@ -49964,24 +50911,7 @@ function buildMcpPolicy(fileConfig) {
|
|
|
49964
50911
|
};
|
|
49965
50912
|
}
|
|
49966
50913
|
function registerMcpTools(registry2, policy) {
|
|
49967
|
-
const
|
|
49968
|
-
const byWireName = new Map(base.map((t2) => [t2.name, t2]));
|
|
49969
|
-
const wrapped = [];
|
|
49970
|
-
for (const { server, tool } of registry2.tools()) {
|
|
49971
|
-
if (!canEncodeMcpWireName(server, tool.name)) continue;
|
|
49972
|
-
const name = mcpWireName(server, tool.name);
|
|
49973
|
-
const original = byWireName.get(name);
|
|
49974
|
-
if (!original) continue;
|
|
49975
|
-
const address = mcpToolAddress(server, tool.name);
|
|
49976
|
-
wrapped.push({
|
|
49977
|
-
...original,
|
|
49978
|
-
async run(args, ctx) {
|
|
49979
|
-
const decision = mcpCallAllowed(address, policy);
|
|
49980
|
-
if (!decision.ok) return `Error: ${decision.reason}`;
|
|
49981
|
-
return original.run(args, ctx);
|
|
49982
|
-
}
|
|
49983
|
-
});
|
|
49984
|
-
}
|
|
50914
|
+
const wrapped = gatedAgentTools(registry2, policy);
|
|
49985
50915
|
for (const tool of wrapped) registerTool(tool);
|
|
49986
50916
|
return () => {
|
|
49987
50917
|
for (const tool of wrapped) unregisterTool(tool.name);
|