@corenel/sidecar 0.8.6 → 0.8.8
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 +1950 -1053
- package/package.json +9 -7
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);
|
|
@@ -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;
|
|
@@ -18536,6 +18766,66 @@ ${firstUserMsg.content}`;
|
|
|
18536
18766
|
});
|
|
18537
18767
|
|
|
18538
18768
|
// ../tools-node/promptHost.ts
|
|
18769
|
+
function backend(svc) {
|
|
18770
|
+
return {
|
|
18771
|
+
async readFile(p2) {
|
|
18772
|
+
try {
|
|
18773
|
+
return await svc.read(normalizeFilePath(p2));
|
|
18774
|
+
} catch {
|
|
18775
|
+
return null;
|
|
18776
|
+
}
|
|
18777
|
+
},
|
|
18778
|
+
async isDirectory(p2) {
|
|
18779
|
+
return (await svc.stat(normalizeFilePath(p2)))?.kind === "directory";
|
|
18780
|
+
},
|
|
18781
|
+
async readdir(p2) {
|
|
18782
|
+
return (await svc.list(normalizeFilePath(p2))).map((n2) => n2.name);
|
|
18783
|
+
}
|
|
18784
|
+
};
|
|
18785
|
+
}
|
|
18786
|
+
async function preload(svc) {
|
|
18787
|
+
const out = {};
|
|
18788
|
+
for (const f2 of flattenFiles(await svc.list())) {
|
|
18789
|
+
try {
|
|
18790
|
+
out[f2.path] = await svc.read(f2.path);
|
|
18791
|
+
} catch {
|
|
18792
|
+
}
|
|
18793
|
+
}
|
|
18794
|
+
return out;
|
|
18795
|
+
}
|
|
18796
|
+
async function compile(src2, values, fileSystem, mainPath) {
|
|
18797
|
+
const mp = mainPath || "/main.prmd";
|
|
18798
|
+
const fs17 = fileSystem || new MemoryFileSystem({ [mp]: src2 });
|
|
18799
|
+
const ctx = await compiler.compileWithContext(mp, { parameters: values || {}, fileSystem: fs17, outputFormat: "markdown" });
|
|
18800
|
+
const compiled = ctx.compiledResult;
|
|
18801
|
+
const output = typeof compiled === "string" ? compiled : compiled ? new TextDecoder().decode(compiled) : "";
|
|
18802
|
+
return { output };
|
|
18803
|
+
}
|
|
18804
|
+
function nodePromptHost(workspace, project) {
|
|
18805
|
+
return {
|
|
18806
|
+
workspace,
|
|
18807
|
+
/* The user's PROJECT files, when the caller has them, so skills installed
|
|
18808
|
+
* into `.prompd/skills/` are visible. Omitted rather than faked when there
|
|
18809
|
+
* is none: a CLI invoked outside a project genuinely has no installed
|
|
18810
|
+
* skills, and saying so beats inventing an empty directory. */
|
|
18811
|
+
...project ? { projectWorkspace: () => project } : {},
|
|
18812
|
+
seed: async (defaults2) => {
|
|
18813
|
+
for (const [p2, content] of Object.entries(defaults2)) {
|
|
18814
|
+
if (!await workspace.stat(p2)) await workspace.write(p2, content);
|
|
18815
|
+
}
|
|
18816
|
+
},
|
|
18817
|
+
parse: (source) => ({ meta: parser.parseContent(source).metadata ?? {} }),
|
|
18818
|
+
compileBody: (source, values) => compile(source, values),
|
|
18819
|
+
compileTemplate: async (path, liveSrc, values) => {
|
|
18820
|
+
const sources = await preload(workspace);
|
|
18821
|
+
const p2 = normalizeFilePath(path);
|
|
18822
|
+
sources[p2] = liveSrc;
|
|
18823
|
+
return compile("", values, new HybridFileSystem(sources, backend(workspace)), "/" + p2);
|
|
18824
|
+
},
|
|
18825
|
+
siteIndex: () => ""
|
|
18826
|
+
// no site index in node (the help chat is browser-only)
|
|
18827
|
+
};
|
|
18828
|
+
}
|
|
18539
18829
|
var parser, compiler;
|
|
18540
18830
|
var init_promptHost = __esm({
|
|
18541
18831
|
"../tools-node/promptHost.ts"() {
|
|
@@ -18550,7 +18840,7 @@ var init_promptHost = __esm({
|
|
|
18550
18840
|
// ../tools-node/migrateHome.ts
|
|
18551
18841
|
import { promises as fs5 } from "node:fs";
|
|
18552
18842
|
import { homedir as homedir3 } from "node:os";
|
|
18553
|
-
import { join as
|
|
18843
|
+
import { join as join6, dirname as dirname3 } from "node:path";
|
|
18554
18844
|
async function isDir(p2) {
|
|
18555
18845
|
return fs5.stat(p2).then((s2) => s2.isDirectory()).catch(() => false);
|
|
18556
18846
|
}
|
|
@@ -18566,7 +18856,7 @@ async function moveEntry(from, to, report) {
|
|
|
18566
18856
|
report.skipped.push({ path: to, reason: "destination already exists" });
|
|
18567
18857
|
return;
|
|
18568
18858
|
}
|
|
18569
|
-
await fs5.mkdir(
|
|
18859
|
+
await fs5.mkdir(dirname3(to), { recursive: true });
|
|
18570
18860
|
try {
|
|
18571
18861
|
await fs5.rename(from, to);
|
|
18572
18862
|
report.moved.push(to);
|
|
@@ -18592,8 +18882,8 @@ async function moveEntry(from, to, report) {
|
|
|
18592
18882
|
async function mergeDir(from, to, report) {
|
|
18593
18883
|
const entries = await fs5.readdir(from, { withFileTypes: true }).catch(() => []);
|
|
18594
18884
|
for (const e2 of entries) {
|
|
18595
|
-
const src2 =
|
|
18596
|
-
const dst =
|
|
18885
|
+
const src2 = join6(from, e2.name);
|
|
18886
|
+
const dst = join6(to, e2.name);
|
|
18597
18887
|
if (e2.isDirectory() && await isDir(dst)) await mergeDir(src2, dst, report);
|
|
18598
18888
|
else await moveEntry(src2, dst, report);
|
|
18599
18889
|
}
|
|
@@ -18604,16 +18894,16 @@ async function migrateHomeLayout(base = homedir3()) {
|
|
|
18604
18894
|
const report = { moved: [], skipped: [] };
|
|
18605
18895
|
const home = corenelDir2(base);
|
|
18606
18896
|
if (!await present(home)) return report;
|
|
18607
|
-
const config =
|
|
18897
|
+
const config = join6(home, "config");
|
|
18608
18898
|
if (await isDir(config)) await mergeDir(config, home, report);
|
|
18609
18899
|
const side = sidecarDir(base);
|
|
18610
18900
|
for (const name of SIDECAR_FILES) {
|
|
18611
|
-
await moveEntry(
|
|
18901
|
+
await moveEntry(join6(home, name), join6(side, name), report);
|
|
18612
18902
|
}
|
|
18613
18903
|
for (const name of SIDECAR_DIRS) {
|
|
18614
|
-
const src2 =
|
|
18904
|
+
const src2 = join6(home, name);
|
|
18615
18905
|
if (!await isDir(src2)) continue;
|
|
18616
|
-
const dst =
|
|
18906
|
+
const dst = join6(side, name);
|
|
18617
18907
|
if (await isDir(dst)) await mergeDir(src2, dst, report);
|
|
18618
18908
|
else await moveEntry(src2, dst, report);
|
|
18619
18909
|
}
|
|
@@ -18638,7 +18928,50 @@ var init_migrateHome = __esm({
|
|
|
18638
18928
|
}
|
|
18639
18929
|
});
|
|
18640
18930
|
|
|
18931
|
+
// ../tools-node/openBrowser.ts
|
|
18932
|
+
import { spawn } from "node:child_process";
|
|
18933
|
+
function openBrowser(url) {
|
|
18934
|
+
const [cmd, args] = process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : process.platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
18935
|
+
const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
|
|
18936
|
+
child.on("error", () => {
|
|
18937
|
+
});
|
|
18938
|
+
child.unref();
|
|
18939
|
+
}
|
|
18940
|
+
var init_openBrowser = __esm({
|
|
18941
|
+
"../tools-node/openBrowser.ts"() {
|
|
18942
|
+
"use strict";
|
|
18943
|
+
}
|
|
18944
|
+
});
|
|
18945
|
+
|
|
18641
18946
|
// ../tools-node/index.ts
|
|
18947
|
+
var tools_node_exports = {};
|
|
18948
|
+
__export(tools_node_exports, {
|
|
18949
|
+
API_BASE_ENV: () => API_BASE_ENV,
|
|
18950
|
+
NodeFileService: () => NodeFileService,
|
|
18951
|
+
TOKEN_ENV: () => TOKEN_ENV,
|
|
18952
|
+
authTokenPath: () => authTokenPath,
|
|
18953
|
+
clearAllCredentials: () => clearAllCredentials,
|
|
18954
|
+
clearAuthToken: () => clearAuthToken,
|
|
18955
|
+
corenelDir: () => corenelDir2,
|
|
18956
|
+
credentialDir: () => credentialDir,
|
|
18957
|
+
credentialFilePath: () => credentialFilePath,
|
|
18958
|
+
maskAccount: () => maskAccount,
|
|
18959
|
+
maskToken: () => maskToken,
|
|
18960
|
+
migrateHomeLayout: () => migrateHomeLayout,
|
|
18961
|
+
nodeAuthToken: () => nodeAuthToken,
|
|
18962
|
+
nodePromptHost: () => nodePromptHost,
|
|
18963
|
+
openBrowser: () => openBrowser,
|
|
18964
|
+
readAuthToken: () => readAuthToken,
|
|
18965
|
+
readAuthTokenRead: () => readAuthTokenRead,
|
|
18966
|
+
readCredentialFile: () => readCredentialFile,
|
|
18967
|
+
readCredentialFileRead: () => readCredentialFileRead,
|
|
18968
|
+
resolveApiBase: () => resolveApiBase,
|
|
18969
|
+
resolveNodeCredential: () => resolveNodeCredential,
|
|
18970
|
+
sidecarDir: () => sidecarDir,
|
|
18971
|
+
tokenFilePath: () => tokenFilePath,
|
|
18972
|
+
writeAuthToken: () => writeAuthToken,
|
|
18973
|
+
writeLoginCredential: () => writeLoginCredential
|
|
18974
|
+
});
|
|
18642
18975
|
var init_tools_node = __esm({
|
|
18643
18976
|
"../tools-node/index.ts"() {
|
|
18644
18977
|
"use strict";
|
|
@@ -18646,59 +18979,7 @@ var init_tools_node = __esm({
|
|
|
18646
18979
|
init_auth();
|
|
18647
18980
|
init_promptHost();
|
|
18648
18981
|
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"];
|
|
18982
|
+
init_openBrowser();
|
|
18702
18983
|
}
|
|
18703
18984
|
});
|
|
18704
18985
|
|
|
@@ -25102,270 +25383,6 @@ var init_fileFromPath = __esm({
|
|
|
25102
25383
|
}
|
|
25103
25384
|
});
|
|
25104
25385
|
|
|
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
25386
|
// src/cli.ts
|
|
25370
25387
|
init_protocol();
|
|
25371
25388
|
import { join as join21 } from "node:path";
|
|
@@ -28083,7 +28100,127 @@ function makeFileMirrorSource(deps) {
|
|
|
28083
28100
|
|
|
28084
28101
|
// src/controlPlane.ts
|
|
28085
28102
|
init_protocol();
|
|
28086
|
-
|
|
28103
|
+
|
|
28104
|
+
// ../config/src/daemonConfig.ts
|
|
28105
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
28106
|
+
import { dirname, join as join3 } from "node:path";
|
|
28107
|
+
var DAEMON_CONFIG_VERSION = 1;
|
|
28108
|
+
var SECRET_KEYS = ["acpAuth", "tunnelToken", "token"];
|
|
28109
|
+
var KNOWN_KEYS = [
|
|
28110
|
+
"version",
|
|
28111
|
+
"port",
|
|
28112
|
+
"host",
|
|
28113
|
+
"roots",
|
|
28114
|
+
"addUnder",
|
|
28115
|
+
"cloneUnder",
|
|
28116
|
+
"allowLan",
|
|
28117
|
+
"allowOrigins",
|
|
28118
|
+
"tlsCert",
|
|
28119
|
+
"tlsKey",
|
|
28120
|
+
"selfSigned",
|
|
28121
|
+
"tunnel",
|
|
28122
|
+
"tunnelName",
|
|
28123
|
+
"tunnelHostname",
|
|
28124
|
+
"endpoint",
|
|
28125
|
+
"allowShell",
|
|
28126
|
+
"keepTranscripts",
|
|
28127
|
+
"isolation",
|
|
28128
|
+
"msbBin",
|
|
28129
|
+
"guestImage",
|
|
28130
|
+
"maxChainDepth",
|
|
28131
|
+
"name",
|
|
28132
|
+
"appUrl",
|
|
28133
|
+
"defaultModel",
|
|
28134
|
+
"mcpServers",
|
|
28135
|
+
"mcpAllow"
|
|
28136
|
+
];
|
|
28137
|
+
function daemonConfigPath(stateDir) {
|
|
28138
|
+
return join3(stateDir, "daemon.json");
|
|
28139
|
+
}
|
|
28140
|
+
async function readDaemonConfig(stateDir) {
|
|
28141
|
+
const empty = (corrupt) => ({ config: { version: DAEMON_CONFIG_VERSION }, unknown: {}, corrupt });
|
|
28142
|
+
let raw;
|
|
28143
|
+
try {
|
|
28144
|
+
raw = await readFile(daemonConfigPath(stateDir), "utf8");
|
|
28145
|
+
} catch {
|
|
28146
|
+
return empty(false);
|
|
28147
|
+
}
|
|
28148
|
+
let parsed;
|
|
28149
|
+
try {
|
|
28150
|
+
parsed = JSON.parse(raw);
|
|
28151
|
+
} catch {
|
|
28152
|
+
return empty(true);
|
|
28153
|
+
}
|
|
28154
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return empty(true);
|
|
28155
|
+
const src2 = parsed;
|
|
28156
|
+
const config = { version: DAEMON_CONFIG_VERSION };
|
|
28157
|
+
const unknown = {};
|
|
28158
|
+
for (const [k2, v2] of Object.entries(src2)) {
|
|
28159
|
+
if (k2 === "version") continue;
|
|
28160
|
+
if (KNOWN_KEYS.includes(k2)) {
|
|
28161
|
+
config[k2] = v2;
|
|
28162
|
+
} else {
|
|
28163
|
+
unknown[k2] = v2;
|
|
28164
|
+
}
|
|
28165
|
+
}
|
|
28166
|
+
return { config, unknown, corrupt: false };
|
|
28167
|
+
}
|
|
28168
|
+
async function writeDaemonConfig(stateDir, next, unknown = {}) {
|
|
28169
|
+
const body2 = { ...unknown, ...next, version: DAEMON_CONFIG_VERSION };
|
|
28170
|
+
const path = daemonConfigPath(stateDir);
|
|
28171
|
+
await mkdir(dirname(path), { recursive: true });
|
|
28172
|
+
await writeFile(path, `${JSON.stringify(body2, null, 2)}
|
|
28173
|
+
`, "utf8");
|
|
28174
|
+
}
|
|
28175
|
+
|
|
28176
|
+
// ../config/src/credential.ts
|
|
28177
|
+
init_tools_node();
|
|
28178
|
+
import { promises as fs6 } from "node:fs";
|
|
28179
|
+
var TOKEN_ENV2 = TOKEN_ENV;
|
|
28180
|
+
function authDir(stateDir) {
|
|
28181
|
+
return credentialDir(stateDir);
|
|
28182
|
+
}
|
|
28183
|
+
function credentialPath(stateDir) {
|
|
28184
|
+
return credentialFilePath(stateDir);
|
|
28185
|
+
}
|
|
28186
|
+
var STORED_CREDENTIAL_KEYS = ["accessToken", "refreshToken", "expiresAt", "account", "apiBase"];
|
|
28187
|
+
async function readForeignAuthJson(path) {
|
|
28188
|
+
let raw;
|
|
28189
|
+
try {
|
|
28190
|
+
raw = await fs6.readFile(path, "utf8");
|
|
28191
|
+
} catch {
|
|
28192
|
+
return {};
|
|
28193
|
+
}
|
|
28194
|
+
try {
|
|
28195
|
+
const parsed = JSON.parse(raw);
|
|
28196
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
28197
|
+
const obj = { ...parsed };
|
|
28198
|
+
for (const k2 of STORED_CREDENTIAL_KEYS) delete obj[k2];
|
|
28199
|
+
return obj;
|
|
28200
|
+
} catch {
|
|
28201
|
+
return {};
|
|
28202
|
+
}
|
|
28203
|
+
}
|
|
28204
|
+
async function saveCredential(cred, stateDir) {
|
|
28205
|
+
const path = credentialPath(stateDir);
|
|
28206
|
+
await fs6.mkdir(authDir(stateDir), { recursive: true });
|
|
28207
|
+
const foreign = await readForeignAuthJson(path);
|
|
28208
|
+
const next = { ...foreign, ...cred };
|
|
28209
|
+
await fs6.writeFile(path, JSON.stringify(next, null, 2) + "\n", { mode: 384 });
|
|
28210
|
+
try {
|
|
28211
|
+
await fs6.chmod(path, 384);
|
|
28212
|
+
} catch {
|
|
28213
|
+
}
|
|
28214
|
+
await writeAuthToken(cred.accessToken, stateDir);
|
|
28215
|
+
}
|
|
28216
|
+
async function clearCredential(stateDir) {
|
|
28217
|
+
await clearAllCredentials(stateDir);
|
|
28218
|
+
}
|
|
28219
|
+
async function resolveCredential(env = process.env, stateDir, now2 = Date.now) {
|
|
28220
|
+
return await resolveNodeCredential({ env, now: now2, ...stateDir ? { stateDir } : {} });
|
|
28221
|
+
}
|
|
28222
|
+
|
|
28223
|
+
// src/controlPlane.ts
|
|
28087
28224
|
function describeBackend(url, mirrors, cred, probe) {
|
|
28088
28225
|
const detail = probe ? {
|
|
28089
28226
|
reachable: probe.answered,
|
|
@@ -28143,8 +28280,8 @@ function makeControlPlane(deps) {
|
|
|
28143
28280
|
|
|
28144
28281
|
// src/tools.ts
|
|
28145
28282
|
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
|
|
28283
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
28284
|
+
import { resolve as resolve2, relative as relative2, isAbsolute as isAbsolute2, sep as sep2, dirname as dirname4 } from "node:path";
|
|
28148
28285
|
function withinRoot(root, abs) {
|
|
28149
28286
|
const rel = relative2(root, abs);
|
|
28150
28287
|
return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel) && rel.split(sep2)[0] !== "..";
|
|
@@ -28172,7 +28309,7 @@ async function assertRealWithinRoot(roots, abs) {
|
|
|
28172
28309
|
return;
|
|
28173
28310
|
} catch (e2) {
|
|
28174
28311
|
if (e2 instanceof Error && e2.message.includes("via symlink")) throw e2;
|
|
28175
|
-
const parent =
|
|
28312
|
+
const parent = dirname4(probe);
|
|
28176
28313
|
if (parent === probe) throw new Error(`cannot resolve path under the sidecar roots: ${abs}`);
|
|
28177
28314
|
probe = parent;
|
|
28178
28315
|
}
|
|
@@ -28223,7 +28360,7 @@ function nodeFsTools(roots) {
|
|
|
28223
28360
|
mutates: true,
|
|
28224
28361
|
async run(args) {
|
|
28225
28362
|
const abs = confine(roots, args.root, String(args.path ?? ""));
|
|
28226
|
-
await fs7.mkdir(
|
|
28363
|
+
await fs7.mkdir(dirname4(abs), { recursive: true });
|
|
28227
28364
|
await assertRealWithinRoot(roots, abs);
|
|
28228
28365
|
await fs7.writeFile(abs, String(args.content ?? ""), "utf8");
|
|
28229
28366
|
return `wrote ${args.path}`;
|
|
@@ -28274,7 +28411,7 @@ function nodeFsTools(roots) {
|
|
|
28274
28411
|
if (bytes.byteLength > MAX_READ_BYTES) {
|
|
28275
28412
|
throw new Error(`file too large to write (${bytes.byteLength} bytes; max ${MAX_READ_BYTES})`);
|
|
28276
28413
|
}
|
|
28277
|
-
await fs7.mkdir(
|
|
28414
|
+
await fs7.mkdir(dirname4(abs), { recursive: true });
|
|
28278
28415
|
await assertRealWithinRoot(roots, abs);
|
|
28279
28416
|
await fs7.writeFile(abs, bytes);
|
|
28280
28417
|
return `wrote ${args.path} (${bytes.byteLength} bytes)`;
|
|
@@ -28389,7 +28526,7 @@ function runShell(command, cwd, timeout) {
|
|
|
28389
28526
|
return new Promise((resolveOut) => {
|
|
28390
28527
|
const isWin = process.platform === "win32";
|
|
28391
28528
|
const [shell, shellArgs] = isWin ? ["cmd.exe", ["/d", "/s", "/c", command]] : ["/bin/sh", ["-c", command]];
|
|
28392
|
-
const child =
|
|
28529
|
+
const child = spawn2(shell, shellArgs, { cwd, windowsHide: true });
|
|
28393
28530
|
let out = "";
|
|
28394
28531
|
let truncated = false;
|
|
28395
28532
|
let timedOut = false;
|
|
@@ -28424,7 +28561,7 @@ ${truncated ? "[output truncated]\n" : ""}${body2}`);
|
|
|
28424
28561
|
// src/configService.ts
|
|
28425
28562
|
init_protocol();
|
|
28426
28563
|
import { promises as fs8 } from "node:fs";
|
|
28427
|
-
import { resolve as resolve3, relative as relative3, join as
|
|
28564
|
+
import { resolve as resolve3, relative as relative3, join as join7, dirname as dirname5, sep as sep3 } from "node:path";
|
|
28428
28565
|
var MAX_LIST_DEPTH2 = 24;
|
|
28429
28566
|
var MAX_LIST_ENTRIES2 = 2e4;
|
|
28430
28567
|
function relUnder(root, abs) {
|
|
@@ -28459,7 +28596,7 @@ var NodeConfigService = class {
|
|
|
28459
28596
|
real = await fs8.realpath(probe);
|
|
28460
28597
|
} catch {
|
|
28461
28598
|
if (mustExist && probe === abs) throw new Error(`no such harness config file: ${relUnder(this.stateDir, abs) ?? abs}`);
|
|
28462
|
-
const parent =
|
|
28599
|
+
const parent = dirname5(probe);
|
|
28463
28600
|
if (parent === probe) throw new Error("cannot resolve a path inside the harness config");
|
|
28464
28601
|
probe = parent;
|
|
28465
28602
|
continue;
|
|
@@ -28495,11 +28632,11 @@ var NodeConfigService = class {
|
|
|
28495
28632
|
async listRoot(levels) {
|
|
28496
28633
|
const out = [];
|
|
28497
28634
|
for (const name of CONFIG_DEFINITION_FILES) {
|
|
28498
|
-
const st2 = await fs8.stat(
|
|
28635
|
+
const st2 = await fs8.stat(join7(this.stateDir, name)).catch(() => null);
|
|
28499
28636
|
if (st2?.isFile()) out.push({ path: name, size: st2.size, mtime: st2.mtimeMs });
|
|
28500
28637
|
}
|
|
28501
28638
|
for (const name of CONFIG_DEFINITION_DIRS) {
|
|
28502
|
-
const abs =
|
|
28639
|
+
const abs = join7(this.stateDir, name);
|
|
28503
28640
|
const st2 = await fs8.stat(abs).catch(() => null);
|
|
28504
28641
|
if (!st2?.isDirectory()) continue;
|
|
28505
28642
|
out.push({ path: name, dir: true });
|
|
@@ -28514,7 +28651,7 @@ var NodeConfigService = class {
|
|
|
28514
28651
|
for (const e2 of entries) {
|
|
28515
28652
|
if (out.length >= MAX_LIST_ENTRIES2) return;
|
|
28516
28653
|
if (e2.isSymbolicLink()) continue;
|
|
28517
|
-
const child =
|
|
28654
|
+
const child = join7(abs, e2.name);
|
|
28518
28655
|
if (e2.isDirectory()) {
|
|
28519
28656
|
out.push({ path: `${prefix}${e2.name}`, dir: true });
|
|
28520
28657
|
await this.walk(child, `${prefix}${e2.name}/`, levels - 1, out);
|
|
@@ -28533,7 +28670,7 @@ var NodeConfigService = class {
|
|
|
28533
28670
|
* partially written AGENT.md is a definition that parses to nonsense. */
|
|
28534
28671
|
async write(path, content) {
|
|
28535
28672
|
const abs = await this.confine(path, false);
|
|
28536
|
-
await fs8.mkdir(
|
|
28673
|
+
await fs8.mkdir(dirname5(abs), { recursive: true });
|
|
28537
28674
|
await this.assertRealAllowed(abs, false);
|
|
28538
28675
|
const tmp = `${abs}.${process.pid}.${Date.now()}.tmp`;
|
|
28539
28676
|
try {
|
|
@@ -28552,7 +28689,7 @@ var NodeConfigService = class {
|
|
|
28552
28689
|
};
|
|
28553
28690
|
|
|
28554
28691
|
// src/procs.ts
|
|
28555
|
-
import { spawn as
|
|
28692
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
28556
28693
|
var MAX_RUNNING = 8;
|
|
28557
28694
|
var MAX_CHUNKS = 400;
|
|
28558
28695
|
var MAX_CHARS = 2e5;
|
|
@@ -28614,7 +28751,7 @@ function createNodeProcs(opts) {
|
|
|
28614
28751
|
const isWin = process.platform === "win32";
|
|
28615
28752
|
const shell = isWin ? "cmd.exe" : "/bin/sh";
|
|
28616
28753
|
const shellArgs = isWin ? ["/d", "/s", "/c", line] : ["-c", line];
|
|
28617
|
-
const child =
|
|
28754
|
+
const child = spawn3(shell, shellArgs, {
|
|
28618
28755
|
cwd: dir,
|
|
28619
28756
|
windowsHide: true,
|
|
28620
28757
|
stdio: "pipe",
|
|
@@ -29552,6 +29689,96 @@ var todoWriteTool = {
|
|
|
29552
29689
|
}
|
|
29553
29690
|
};
|
|
29554
29691
|
|
|
29692
|
+
// ../harness/tools/project.ts
|
|
29693
|
+
init_protocol();
|
|
29694
|
+
var manifestSuffix = () => `${stateDirName()}/craft/project.json`;
|
|
29695
|
+
async function findProjects(ctx) {
|
|
29696
|
+
const files = ctx.files;
|
|
29697
|
+
if (!files) return [];
|
|
29698
|
+
const suffix = manifestSuffix();
|
|
29699
|
+
const nodes = await files.list().catch(() => []);
|
|
29700
|
+
const out = [];
|
|
29701
|
+
for (const node of flattenFiles(nodes)) {
|
|
29702
|
+
const p2 = node.path;
|
|
29703
|
+
const root = p2 === suffix ? "" : p2.endsWith(`/${suffix}`) ? p2.slice(0, -(suffix.length + 1)) : null;
|
|
29704
|
+
if (root === null) continue;
|
|
29705
|
+
try {
|
|
29706
|
+
const m2 = JSON.parse(await files.read(p2));
|
|
29707
|
+
if (m2 && m2.version === 1 && typeof m2.name === "string") out.push({ root, name: m2.name });
|
|
29708
|
+
} catch {
|
|
29709
|
+
}
|
|
29710
|
+
}
|
|
29711
|
+
return out;
|
|
29712
|
+
}
|
|
29713
|
+
function projectSlug(name) {
|
|
29714
|
+
const s2 = name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
29715
|
+
return s2 || "project";
|
|
29716
|
+
}
|
|
29717
|
+
async function freeRoot(ctx, name) {
|
|
29718
|
+
const files = ctx.files;
|
|
29719
|
+
const base = projectSlug(name);
|
|
29720
|
+
let root = base;
|
|
29721
|
+
for (let i2 = 2; files && await files.stat(root) !== null; i2++) root = `${base}-${i2}`;
|
|
29722
|
+
return root;
|
|
29723
|
+
}
|
|
29724
|
+
var startProjectTool = {
|
|
29725
|
+
name: "start_project",
|
|
29726
|
+
/* DECLARED, like ask_user's, and for the same reason: its whole effect is to
|
|
29727
|
+
* put a choice in front of someone who is already there, and the write it
|
|
29728
|
+
* may then make is the thing they just chose. A permission prompt on top of
|
|
29729
|
+
* the choice is two dialogs for one decision. */
|
|
29730
|
+
permission: "allow",
|
|
29731
|
+
noOffload: true,
|
|
29732
|
+
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.",
|
|
29733
|
+
parameters: {
|
|
29734
|
+
type: "object",
|
|
29735
|
+
properties: {
|
|
29736
|
+
name: { type: "string", description: "Proposed project name, 2-4 words, derived from the request." },
|
|
29737
|
+
reason: { type: "string", description: "One short line: what is about to be written into it." }
|
|
29738
|
+
},
|
|
29739
|
+
required: ["name"]
|
|
29740
|
+
},
|
|
29741
|
+
async run(args, ctx) {
|
|
29742
|
+
const files = ctx.files;
|
|
29743
|
+
if (!files) return "start_project is unavailable: no file workspace is connected.";
|
|
29744
|
+
if (!ctx.ask) return "start_project is unavailable in this environment (nobody can be asked).";
|
|
29745
|
+
if (!files.writable) return "This workspace is read-only, so no project can be created in it.";
|
|
29746
|
+
const proposed = (typeof args.name === "string" ? args.name : "").trim() || "Untitled project";
|
|
29747
|
+
const existing = await findProjects(ctx);
|
|
29748
|
+
const createLabel = `Create "${proposed}"`;
|
|
29749
|
+
const options = [
|
|
29750
|
+
{ label: createLabel, description: typeof args.reason === "string" && args.reason ? args.reason : "A new project for this work." },
|
|
29751
|
+
...existing.slice(0, 3).map((p2) => ({ label: p2.name, description: `Put it in the existing project at ${p2.root || "the workspace root"}.` }))
|
|
29752
|
+
];
|
|
29753
|
+
if (options.length < 2) options.push({ label: "Not now", description: "Write nothing; the document has nowhere to live yet." });
|
|
29754
|
+
const canMove = !!ctx.moveSessionToProject;
|
|
29755
|
+
const questions = [
|
|
29756
|
+
{ header: "Project", question: "Documents live in a project. Where should this one go?", options },
|
|
29757
|
+
...canMove ? [{
|
|
29758
|
+
header: "This chat",
|
|
29759
|
+
question: "Bring this conversation into the project too?",
|
|
29760
|
+
options: [
|
|
29761
|
+
{ label: "Yes, move it", description: "The transcript moves with the work, and this chat carries on." },
|
|
29762
|
+
{ label: "Keep it loose", description: "The document goes in the project; this chat stays where it is." }
|
|
29763
|
+
]
|
|
29764
|
+
}] : []
|
|
29765
|
+
];
|
|
29766
|
+
const answers = await ctx.ask(questions);
|
|
29767
|
+
const picked = answers ? answers.Project?.[0] : void 0;
|
|
29768
|
+
const move = answers ? answers["This chat"]?.[0] === "Yes, move it" : false;
|
|
29769
|
+
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.";
|
|
29770
|
+
if (picked === "Not now") return "The person declined to start a project. Do not write the document.";
|
|
29771
|
+
const moved = async (root2) => move && ctx.moveSessionToProject ? ` ${await ctx.moveSessionToProject(root2)}` : "";
|
|
29772
|
+
const hit = existing.find((p2) => p2.name === picked);
|
|
29773
|
+
if (hit) return `Use the existing project "${hit.name}". Write the document inside ${hit.root || "the workspace root"}/.${await moved(hit.root)}`;
|
|
29774
|
+
const name = picked === createLabel ? proposed : picked;
|
|
29775
|
+
const root = await freeRoot(ctx, name);
|
|
29776
|
+
const manifest = { name, createdAt: Date.now(), version: 1 };
|
|
29777
|
+
await files.write(`${root}/${manifestSuffix()}`, JSON.stringify(manifest, null, 2));
|
|
29778
|
+
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)}`;
|
|
29779
|
+
}
|
|
29780
|
+
};
|
|
29781
|
+
|
|
29555
29782
|
// ../harness/tools/sessions.ts
|
|
29556
29783
|
function str3(args, key) {
|
|
29557
29784
|
const v2 = args[key];
|
|
@@ -30133,6 +30360,7 @@ var defaultTools = [
|
|
|
30133
30360
|
webSearchTool,
|
|
30134
30361
|
todoWriteTool,
|
|
30135
30362
|
compilePrompdTool,
|
|
30363
|
+
startProjectTool,
|
|
30136
30364
|
...fileTools
|
|
30137
30365
|
];
|
|
30138
30366
|
function allTools() {
|
|
@@ -30212,10 +30440,10 @@ Add a constraint :: Add a constraint or rule to the prompt instructions (what to
|
|
|
30212
30440
|
`,
|
|
30213
30441
|
"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
30442
|
"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:
|
|
30443
|
+
"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",
|
|
30444
|
+
"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",
|
|
30445
|
+
"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",
|
|
30446
|
+
"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
30447
|
"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
30448
|
"crew/room.md": `---
|
|
30221
30449
|
id: crew-room
|
|
@@ -30350,7 +30578,7 @@ You are a privacy reviewer. Determine whether the INPUT exposes personally ident
|
|
|
30350
30578
|
id: system-base
|
|
30351
30579
|
name: Operational system prompt
|
|
30352
30580
|
version: 1.0.0
|
|
30353
|
-
description: The agent's system prompt \u2014
|
|
30581
|
+
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
30582
|
parameters:
|
|
30355
30583
|
- name: persona
|
|
30356
30584
|
type: string
|
|
@@ -30383,9 +30611,11 @@ parameters:
|
|
|
30383
30611
|
---
|
|
30384
30612
|
{% if persona %}{{ persona }}
|
|
30385
30613
|
|
|
30386
|
-
{% endif %}You are the {{ app }} assistant
|
|
30614
|
+
{% endif %}You are the {{ app }} assistant. You help the user do real work in {{ app }}: reading and changing files, composing and running prompts and workflows, and building software. What is in front of you differs by surface \u2014 an open file, a workflow canvas, a workspace, or nothing but this conversation \u2014 so work from the context you were actually given rather than assuming a particular screen.
|
|
30387
30615
|
|
|
30388
|
-
To
|
|
30616
|
+
To read or change files in the workspace, use the file tools; writes ask the user for permission first. Use compile_prompd to preview rendered output and search_packages to find registry packages. For any multi-step job, maintain a visible task list with todo_write: lay out the steps, keep one task in_progress, and mark each completed as you go. Prefer calling a tool over guessing, and be concise.
|
|
30617
|
+
|
|
30618
|
+
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.
|
|
30389
30619
|
|
|
30390
30620
|
## Tools available this run
|
|
30391
30621
|
{{ tools }}
|
|
@@ -30461,7 +30691,7 @@ what you will do before you do it unattended.
|
|
|
30461
30691
|
"systems/system-prmd-md.md": '---\nid: system-prmd-md\nname: Prompt file system prompt (.prmd / .md)\nversion: 1.0.0\ninherits: ./system-base.md\nparameters:\n - name: persona\n type: string\n description: The selected persona\'s voice/role text, placed first.\n default: ""\n - name: mode\n type: string\n description: Active mode id (auto / edit / plan / brainstorm).\n default: "auto"\n - name: mode_hint\n type: string\n description: Steering text for the active mode.\n default: ""\n - name: tools\n type: string\n description: Definitions of the tools available this run.\n default: ""\n - name: tool_guidance\n type: string\n description: Optional extra guidance on tool usage.\n default: ""\n - name: file_context\n type: string\n description: The editor file / selection / compiled-output context block.\n default: ""\n---\n\n{% include "../languages/prmd.md" %}\n\nWhen you edit this file, call `propose_edit` with the COMPLETE new file content. Keep the frontmatter valid YAML and only reference declared parameters in the body.\n',
|
|
30462
30692
|
"templates/index.json": '[\n {\n "_comment": "EXAMPLE of a data-driven New File type. Copy this entry, remove \\"disabled\\", give it a unique \\"id\\", and create the seed file at \\"template\\" (in this templates/ folder). Tokens __ID__ / __NAME__ are substituted on create. Icon is a ui-kit name (e.g. IcFileCode, IcUser, IcFlow) or \\"svg:<markup>\\". See docs/file-types.md.",\n "disabled": true,\n "id": "example-agent-prompt",\n "label": "Agent prompt",\n "description": "A .prmd tuned for tool-using agents.",\n "ext": "prmd",\n "category": "Prompd",\n "icon": "IcUser",\n "defaultName": "agent",\n "template": "templates/new-agent-prmd.prmd",\n "accent": true\n }\n]\n',
|
|
30463
30693
|
"templates/new-blank-prmd-md.prmd": "---\nid: __ID__\nname: __NAME__\nversion: 1.0.0\n---\n\n",
|
|
30464
|
-
"templates/new-default-pdflow.pdflow": '{\n "version": "1.0.0",\n "metadata": {\n "id": "__ID__",\n "name": "__NAME__"\n },\n "nodes": [],\n "
|
|
30694
|
+
"templates/new-default-pdflow.pdflow": '{\n "version": "1.0.0",\n "metadata": {\n "id": "__ID__",\n "name": "__NAME__"\n },\n "nodes": [],\n "edges": []\n}\n',
|
|
30465
30695
|
"templates/new-default-prmd-md.prmd": '---\nid: __ID__\nname: __NAME__\nversion: 1.0.0\nparameters:\n - name: topic\n type: string\n default: ""\n description: The subject to write about.\n---\n\nWrite a short, clear answer about {{ topic }}.\n',
|
|
30466
30696
|
"templates/new-default-test-prmd.prmd": `---
|
|
30467
30697
|
id: __ID__
|
|
@@ -30603,7 +30833,147 @@ the goal's stakes and vagueness:
|
|
|
30603
30833
|
"roles/senior-software-engineer/ROLE.md": "---\nlabel: Senior Software Engineer\njobTitle: Senior Software Engineer\ndescription: Implements features and fixes with tests; reads before writing.\nsuggestedSkills: []\n---\nImplement changes test-first. Read the surrounding code and match its conventions. Keep changes minimal and focused; explain non-obvious decisions.\n",
|
|
30604
30834
|
"roles/senior-test-engineer/PERSONA.md": "Meticulous, adversarial about correctness. Thinks in failure modes and boundary conditions.\n",
|
|
30605
30835
|
"roles/senior-test-engineer/POLICY.yaml": "# A tool NOT named below falls through to mutatingDefault (if it mutates) or\n# defaultPermission. In an unattended daemon run there is nobody to prompt, so\n# `ask` resolves to `deny` -- an omitted mutating tool is simply unavailable,\n# with nothing here saying so. Name what the agent needs.\nversion: 1\nname: role:senior-test-engineer\ndefaultPermission: ask\nmutatingDefault: deny\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n compile_prompd: allow\n # The agent's own state: it is told to keep durable notes and, when\n # self-directed, to schedule its own next wake. Both mutate, and this role's\n # backstop is `deny`, so without these it could do neither.\n save_memory: allow\n recall_memory: allow\n kv_get: allow\n kv_set: allow\n schedule_self: allow\n # Room participation. Both are offered only inside a collaboration room, and a\n # room is convened deliberately by a person -- so, like schedule_self, the\n # offer is the authorisation. Without them an agent in a room can neither hand\n # over the floor nor end it, and every room runs to its turn cap, which the\n # room design calls a design failure rather than an ending.\n address_participant: allow\n close_room: allow\n # This role does not modify production source (see ROLE.md), so writes stay\n # on the deny backstop. Named here only where the level would otherwise be\n # guessed at.\n call_agent: deny\n",
|
|
30606
|
-
"roles/senior-test-engineer/ROLE.md": "---\nlabel: Senior Test Engineer\njobTitle: Senior Test Engineer\ndescription: Writes and runs tests; hunts edge cases; does not modify source.\nsuggestedSkills: []\n---\nFocus on coverage and edge cases. Write tests that fail before a fix and pass after. Do not modify production source; report gaps you find.\n"
|
|
30836
|
+
"roles/senior-test-engineer/ROLE.md": "---\nlabel: Senior Test Engineer\njobTitle: Senior Test Engineer\ndescription: Writes and runs tests; hunts edge cases; does not modify source.\nsuggestedSkills: []\n---\nFocus on coverage and edge cases. Write tests that fail before a fix and pass after. Do not modify production source; report gaps you find.\n",
|
|
30837
|
+
"crew/data-analyst/AGENT.md": "---\nname: data-analyst\njobTitle: Data Analyst\ndescription: Reads data carefully, reports what it supports, and names the caveat that changes how it should be read.\ntriggers:\n - type: manual\n---\nUnderstand the data before analysing it. What is one row? Over what period? What is\nmissing, and is it missing at random? An analysis built on a misread schema is\nconfidently wrong, which is the expensive kind.\n\nAnswer the question that was asked, and say when it is the wrong question. If the\ndata cannot support the decision being made, say that first - before the analysis\nthat appears to support it.\n\nQuantify plainly. Give the number, the denominator and the period together. A\npercentage without its base, or a change without its window, invites the reader to\nsupply the flattering interpretation.\n\nState the caveat that would change the conclusion. Small samples, survivorship,\nseasonality, a definition that changed partway through. One honest caveat is worth\nmore than a page of hedging.\n\nDo not infer cause from a comparison. Say what the data shows and what would be\nneeded to establish why. When someone asks for the cause, name the experiment or\nthe additional data that would answer it.\n",
|
|
30838
|
+
"crew/data-analyst/PERSONA.md": "Numerate and concrete. States the number, then what it does and does not mean.\n",
|
|
30839
|
+
"crew/data-analyst/POLICY.yaml": "onViolation: stop\ndefaultPermission: ask\nmutatingDefault: ask\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n compile_prompd: allow\n web_search: allow\n save_memory: allow\n recall_memory: allow\n kv_get: allow\n kv_set: allow\nunattended: deny\n",
|
|
30840
|
+
"crew/data-analyst/SOUL.md": "Careful and unexcitable. Finds the interesting result suspicious until it survives\na second look.\n",
|
|
30841
|
+
"crew/document-writer/AGENT.md": "---\nname: document-writer\njobTitle: Document Writer\ndescription: Drafts and edits documentation grounded in the source, written for the reader who has to act on it.\ntriggers:\n - type: manual\n---\nDecide who is reading and what they need to do next. A reference for someone\nmid-task and an explanation for someone deciding whether to care are different\ndocuments. Writing one document for both readers serves neither.\n\nGround every claim in the source. Read the code, the config, the actual behaviour -\nthen describe it. Documentation that describes intent rather than behaviour is how\na system and its manual quietly diverge.\n\nLead with the thing the reader came for. Put the answer, the command, or the\ndecision first, and the background after it. Most readers stop early; let them\nstop having got what they needed.\n\nPrefer the concrete. One real example beats three sentences of description. Show\nthe actual command, the actual output, the actual file path.\n\nFlag what you could not verify. If you cannot tell whether something is still true,\nsay so in the draft rather than writing around it. A marked uncertainty gets\nchecked; a smoothed-over one ships.\n",
|
|
30842
|
+
"crew/document-writer/PERSONA.md": "Plain and unhurried. Short sentences, concrete nouns, no throat-clearing.\n",
|
|
30843
|
+
"crew/document-writer/POLICY.yaml": "onViolation: stop\ndefaultPermission: ask\nmutatingDefault: ask\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n compile_prompd: allow\n web_search: allow\n save_memory: allow\n recall_memory: allow\n kv_get: allow\n kv_set: allow\nunattended: deny\n",
|
|
30844
|
+
"crew/document-writer/SOUL.md": "Believes documentation is a kindness to a stranger under time pressure, and writes\nfor that person rather than for the author.\n",
|
|
30845
|
+
"crew/marketing-strategist/AGENT.md": "---\nname: marketing-strategist\njobTitle: Marketing Strategist\ndescription: Works out who the audience is, what they already believe, and what would actually change their mind.\ntriggers:\n - type: manual\n---\nStart with the audience, not the product. Who specifically is this for, what are\nthey trying to get done, and what are they doing instead today? A message written\nbefore that is answered is decoration.\n\nName what the audience already believes. Positioning is the work of moving someone\nfrom what they currently think to something else. You cannot plan that move\nwithout stating the starting point.\n\nSay what the thing is not for. A claim that fits everyone persuades no one, and an\nhonest boundary is more credible than a broader promise. Name who should keep doing\nwhat they are doing.\n\nMake claims a sceptic could check. Prefer a specific, verifiable statement to a\nsuperlative. Where you lack evidence for a claim, say so and mark it as a claim to\nbe substantiated rather than dressing it up.\n\nGive the reasoning, not only the recommendation. Say what you assumed about the\nmarket and the audience, so someone who knows better can correct the assumption\ninstead of discarding the whole plan.\n",
|
|
30846
|
+
"crew/marketing-strategist/PERSONA.md": "Direct and commercially minded. Argues from the audience's position rather than the\nseller's.\n",
|
|
30847
|
+
"crew/marketing-strategist/POLICY.yaml": "onViolation: stop\ndefaultPermission: ask\nmutatingDefault: ask\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n compile_prompd: allow\n web_search: allow\n save_memory: allow\n recall_memory: allow\n kv_get: allow\n kv_set: allow\nunattended: deny\n",
|
|
30848
|
+
"crew/marketing-strategist/SOUL.md": "Allergic to language that sounds impressive and says nothing. Would rather make one\nsharp claim that some people reject than a broad one nobody remembers.\n",
|
|
30849
|
+
"crew/research-analyst/AGENT.md": '---\nname: research-analyst\njobTitle: Research Analyst\ndescription: Gathers and weighs sources on a question, and reports what is supported separately from what is inferred.\ntriggers:\n - type: manual\n---\nPin down the question first. Vague questions produce confident, useless answers.\nRestate what is being asked, name the part you will answer, and say what you are\ntreating as out of scope.\n\nSearch before you answer, and search more than once. One query returns one\nneighbourhood of an argument. Look for the strongest case against your emerging\nanswer, not only for support of it.\n\nAttribute every claim. Say where each load-bearing fact came from and when it was\npublished. Where sources disagree, report the disagreement rather than silently\nchoosing a winner.\n\nSeparate what is supported from what you inferred. Both are useful; conflating\nthem is how a guess acquires the authority of a citation. Mark the inferences.\n\nSay how confident you are and what would change your mind. When the evidence is\nthin, say it is thin. "I could not find good sources on this" is a finding, and\nfar more useful than a fluent paragraph with nothing under it.\n',
|
|
30850
|
+
"crew/research-analyst/PERSONA.md": 'Measured and precise about certainty. Distinguishes "the source says" from "this\nsuggests" in the sentence itself.\n',
|
|
30851
|
+
"crew/research-analyst/POLICY.yaml": "onViolation: stop\ndefaultPermission: ask\nmutatingDefault: ask\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n compile_prompd: allow\n web_search: allow\n save_memory: allow\n recall_memory: allow\n kv_get: allow\n kv_set: allow\nunattended: deny\n",
|
|
30852
|
+
"crew/research-analyst/SOUL.md": "Curious and hard to satisfy. Would rather report an honest gap than a tidy answer\nthat does not hold.\n",
|
|
30853
|
+
"crew/senior-software-engineer/AGENT.md": "---\nname: senior-software-engineer\njobTitle: Senior Software Engineer\ndescription: Implements features and fixes test-first, reading the surrounding code before writing any of its own.\ntriggers:\n - type: manual\n---\nRead before you write. Open the files around the change and match what is already\nthere - naming, error handling, test style, how the module is layered. Code that\nreads like its neighbours is worth more than code that is clever.\n\nWork test-first when there is a way to test the thing. Write the failing test,\nwatch it fail for the reason you expect, then make it pass. A test that has never\nfailed has not been shown to test anything.\n\nMake the smallest change that fully solves the problem. Resist the tidy-up that\nwas not asked for; note it instead. A diff that does one thing can be reviewed,\nreverted and understood a year later.\n\nSay plainly what you did not do. Unhandled cases, assumptions you made about\ninputs, a test you could not write - these belong in your summary, not in the\nreader's next incident.\n\nStop and ask when the request is ambiguous in a way that changes the design, when\nthe change would alter behaviour other code depends on, or when you cannot find\nthe code the request refers to. Guessing at any of those costs more than asking.\n",
|
|
30854
|
+
"crew/senior-software-engineer/PERSONA.md": "Pragmatic and terse. Explains a non-obvious decision in one sentence and leaves\nthe obvious ones alone.\n",
|
|
30855
|
+
"crew/senior-software-engineer/POLICY.yaml": "onViolation: stop\ndefaultPermission: ask\nmutatingDefault: ask\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n compile_prompd: allow\n web_search: allow\n save_memory: allow\n recall_memory: allow\n kv_get: allow\n kv_set: allow\nunattended: deny\n",
|
|
30856
|
+
"crew/senior-software-engineer/SOUL.md": "Correctness first, then clarity, then brevity. Unbothered by doing the boring\nversion of a thing when the boring version is right.\n",
|
|
30857
|
+
"crew/senior-test-engineer/AGENT.md": "---\nname: senior-test-engineer\njobTitle: Senior Test Engineer\ndescription: Writes and strengthens tests, and finds the coverage that is missing rather than the coverage that is easy.\ntriggers:\n - type: manual\n---\nFind what is untested, not what is uncovered. A line-coverage number counts lines\nthat ran, not behaviour that was checked. Ask instead which wrong behaviour would\nstill ship green today, and write the test that catches it.\n\nProve every test can fail. After a test passes, change the code it covers and\nconfirm it goes red. A test that passes against a broken implementation is worse\nthan no test, because it buys false confidence.\n\nTest behaviour through the interface a caller uses. Tests bound to internals turn\nevery refactor into a rewrite of the test suite, which is how suites come to be\ndeleted wholesale.\n\nPrefer one clear case over five near-duplicates. Name each test for the behaviour\nit pins, so a failure reads as a sentence about the system rather than a number.\n\nFlag, do not paper over. When code is hard to test, say what shape makes it hard -\nhidden state, a clock, a network call in a constructor. Reaching for heavy mocking\nto get around it hides the design problem and keeps it.\n",
|
|
30858
|
+
"crew/senior-test-engineer/PERSONA.md": "Direct and specific. Names the failure a test prevents rather than describing the\ntest.\n",
|
|
30859
|
+
"crew/senior-test-engineer/POLICY.yaml": "onViolation: stop\ndefaultPermission: ask\nmutatingDefault: ask\npermissions:\n read_file: allow\n list_files: allow\n search_files: allow\n stat_file: allow\n compile_prompd: allow\n web_search: allow\n save_memory: allow\n recall_memory: allow\n kv_get: allow\n kv_set: allow\nunattended: deny\n",
|
|
30860
|
+
"crew/senior-test-engineer/SOUL.md": 'Sceptical in the useful way. Assumes green means "not yet proven wrong" and goes\nlooking for the case nobody wrote down.\n',
|
|
30861
|
+
"workflows/competitor-teardown/workflow.pdflow": `{
|
|
30862
|
+
"version": "1.0",
|
|
30863
|
+
"metadata": {
|
|
30864
|
+
"id": "competitor-teardown",
|
|
30865
|
+
"name": "Competitor Teardown",
|
|
30866
|
+
"description": "Researches each competitor into the same shape, then names where you are differentiated and where you are exposed."
|
|
30867
|
+
},
|
|
30868
|
+
"parameters": [
|
|
30869
|
+
{ "name": "ours", "type": "string", "required": true, "multiline": true, "description": "In a few sentences: what do you sell, to whom, and what do you claim makes it different?" },
|
|
30870
|
+
{ "name": "rivals", "type": "string", "required": true, "multiline": true, "description": "List the competitors to tear down - one name per line." }
|
|
30871
|
+
],
|
|
30872
|
+
"nodes": [
|
|
30873
|
+
{ "id": "start", "type": "trigger", "position": { "x": 0, "y": 0 }, "data": { "label": "Start", "triggerType": "manual" } },
|
|
30874
|
+
{ "id": "each", "type": "loop", "position": { "x": 0, "y": 300 }, "data": { "label": "Profile each competitor", "loopType": "for-each", "items": "{{ rivals }}", "arraySource": "{{ rivals }}", "itemVariable": "rival", "body": ["profile"], "onComplete": "gather", "maxIterations": 12 } },
|
|
30875
|
+
{ "id": "profile", "type": "agent", "parentId": "each", "position": { "x": 260, "y": 300 }, "data": { "label": "Profile one competitor", "systemPrompt": "You profile one competitor at a time into a fixed shape so the profiles can be compared. Search for current information and prefer the company's own words for what they claim. Separate what you found from what you inferred, and say when you could not find something rather than guessing at it.", "userPrompt": "Competitor: {{ rival }}\\n\\nFor context, this is how we position ourselves:\\n{{ ours }}\\n\\nProfile the competitor under exactly these headings: Who they sell to. What they claim. What they actually appear to do well. Where they are weak. How they price, if findable. Keep it under 250 words and mark anything you inferred.", "maxIterations": 8, "outputMode": "final-response" } },
|
|
30876
|
+
{ "id": "profile_tools", "type": "tool", "position": { "x": 520, "y": 300 }, "data": { "label": "Search", "attachedTo": "profile", "tools": ["web_search"] } },
|
|
30877
|
+
{ "id": "gather", "type": "transformer", "position": { "x": 0, "y": 420 }, "data": { "label": "Gather profiles", "mode": "template", "template": "OUR POSITIONING\\n{{ ours }}\\n\\nCOMPETITOR PROFILES\\n{{ each }}" } },
|
|
30878
|
+
{ "id": "gaps", "type": "prompt", "position": { "x": 0, "y": 520 }, "data": { "label": "Differentiation and exposure", "sourceType": "raw", "source": "", "rawPrompt": "Compare the profiles below against our own positioning.\\n\\n{{ input }}\\n\\nSay: where we are genuinely differentiated and which competitor most threatens that; where we are exposed and who exploits it today; and which of our claims a competitor could make just as credibly - that last one matters most, because a claim anyone can make is not positioning. Be blunt. Mark anything resting on an inference rather than a found fact." } },
|
|
30879
|
+
{ "id": "out", "type": "output", "position": { "x": 0, "y": 620 }, "data": { "label": "Teardown" } }
|
|
30880
|
+
],
|
|
30881
|
+
"edges": [
|
|
30882
|
+
{ "id": "e3", "source": "start", "target": "each" },
|
|
30883
|
+
{ "id": "e4", "source": "gather", "target": "gaps" },
|
|
30884
|
+
{ "id": "e5", "source": "gaps", "target": "out" }
|
|
30885
|
+
]
|
|
30886
|
+
}
|
|
30887
|
+
`,
|
|
30888
|
+
"workflows/feedback-to-roadmap-themes/workflow.pdflow": `{
|
|
30889
|
+
"version": "1.0",
|
|
30890
|
+
"metadata": {
|
|
30891
|
+
"id": "feedback-to-roadmap-themes",
|
|
30892
|
+
"name": "Feedback to Roadmap Themes",
|
|
30893
|
+
"description": "Turns a pile of customer feedback into ranked themes, one item at a time, so nothing is lost to a single oversized summary."
|
|
30894
|
+
},
|
|
30895
|
+
"parameters": [
|
|
30896
|
+
{ "name": "feedback", "type": "string", "required": true, "multiline": true, "description": "Paste the feedback - one item per line (a ticket, review, or survey comment each)." }
|
|
30897
|
+
],
|
|
30898
|
+
"nodes": [
|
|
30899
|
+
{ "id": "start", "type": "trigger", "position": { "x": 0, "y": 0 }, "data": { "label": "Start", "triggerType": "manual" } },
|
|
30900
|
+
{ "id": "each", "type": "loop", "position": { "x": 0, "y": 200 }, "data": { "label": "Classify each item", "loopType": "for-each", "items": "{{ feedback }}", "arraySource": "{{ feedback }}", "itemVariable": "entry", "body": ["classify"], "onComplete": "gather", "maxIterations": 60 } },
|
|
30901
|
+
{ "id": "classify", "type": "prompt", "parentId": "each", "position": { "x": 260, "y": 200 }, "data": { "label": "Classify one item", "sourceType": "raw", "source": "", "rawPrompt": "Classify this single piece of customer feedback.\\n\\n{{ entry }}\\n\\nReturn ONLY a JSON object: {\\"theme\\": \\"<short noun phrase>\\", \\"severity\\": \\"low\\"|\\"medium\\"|\\"high\\", \\"quote\\": \\"<the most telling phrase, verbatim>\\"}. Use the customer's own framing for the theme. If the item says nothing actionable, use the theme \\"none\\"." } },
|
|
30902
|
+
{ "id": "gather", "type": "transformer", "position": { "x": 0, "y": 320 }, "data": { "label": "Gather classifications", "mode": "template", "template": "CLASSIFIED FEEDBACK\\n\\n{{ each }}" } },
|
|
30903
|
+
{ "id": "cluster", "type": "prompt", "position": { "x": 0, "y": 420 }, "data": { "label": "Cluster and rank", "sourceType": "raw", "source": "", "rawPrompt": "Merge the classifications below into themes and rank them.\\n\\n{{ input }}\\n\\nCombine themes that are the same thing under different words, and say which you merged. Rank by how often a theme appears WEIGHTED by severity, and show both numbers so the ranking can be argued with. Drop \\"none\\". For each theme give one verbatim customer quote.\\n\\nEnd with what this data cannot tell you - who is missing from it, and what you would need to know before acting on the top theme." } },
|
|
30904
|
+
{ "id": "out", "type": "output", "position": { "x": 0, "y": 520 }, "data": { "label": "Themes" } }
|
|
30905
|
+
],
|
|
30906
|
+
"edges": [
|
|
30907
|
+
{ "id": "e2", "source": "start", "target": "each" },
|
|
30908
|
+
{ "id": "e3", "source": "gather", "target": "cluster" },
|
|
30909
|
+
{ "id": "e4", "source": "cluster", "target": "out" }
|
|
30910
|
+
]
|
|
30911
|
+
}
|
|
30912
|
+
`,
|
|
30913
|
+
"workflows/marketing-strategy-pipeline/workflow.pdflow": `{
|
|
30914
|
+
"version": "1.0",
|
|
30915
|
+
"metadata": {
|
|
30916
|
+
"id": "marketing-strategy-pipeline",
|
|
30917
|
+
"name": "Marketing Strategy Pipeline",
|
|
30918
|
+
"description": "Researches audience, messaging and channel for a product brief in parallel, then turns the three passes into one strategy."
|
|
30919
|
+
},
|
|
30920
|
+
"parameters": [
|
|
30921
|
+
{ "name": "brief", "type": "string", "required": true, "multiline": true, "description": "The product or service this strategy is for - yours or a competitor's - and who you think it is for. The more concrete this is, the less the research has to guess at." }
|
|
30922
|
+
],
|
|
30923
|
+
"nodes": [
|
|
30924
|
+
{ "id": "start", "type": "trigger", "position": { "x": 0, "y": 0 }, "data": { "label": "Start", "triggerType": "manual" } },
|
|
30925
|
+
{ "id": "angles", "type": "parallel", "position": { "x": 220, "y": 0 }, "data": { "label": "Research the three angles", "mode": "fork", "branches": [ { "id": "angles-b1", "label": "Audience", "nodes": ["audience_pass"] }, { "id": "angles-b2", "label": "Messaging", "nodes": ["messaging_pass"] }, { "id": "angles-b3", "label": "Channel", "nodes": ["channel_pass"] } ], "forkCount": 3, "forkLabels": ["Audience", "Messaging", "Channel"], "waitFor": "all", "mergeStrategy": "object", "maxConcurrency": 3 } },
|
|
30926
|
+
{ "id": "collect", "type": "transformer", "position": { "x": 440, "y": 0 }, "data": { "label": "Gather the passes", "mode": "template", "template": "AUDIENCE\\n\\n{{ audience_pass }}\\n\\nMESSAGING\\n\\n{{ messaging_pass }}\\n\\nCHANNEL\\n\\n{{ channel_pass }}\\n\\nORIGINAL BRIEF\\n\\n{{ brief }}" } },
|
|
30927
|
+
{ "id": "strategy", "type": "prompt", "position": { "x": 660, "y": 0 }, "data": { "label": "Write the strategy", "sourceType": "raw", "source": "", "rawPrompt": "Turn the research below into a marketing strategy.\\n\\n{{ input }}\\n\\nWrite: who this is for and who it is not for; the one claim the strategy rests on; the three things to say and where to say them; and what you assumed that someone closer to the market should check. Do not pad. If the research does not support a section, say so instead of inventing it.\\n\\nDo not introduce any capability, channel, metric or price that is not in the brief or the research above. Where the product specifics are unknown, say the strategy rests on the brief alone - do not state an invented specific as the claim it rests on." } },
|
|
30928
|
+
{ "id": "out", "type": "output", "position": { "x": 880, "y": 0 }, "data": { "label": "Strategy" } },
|
|
30929
|
+
{ "id": "audience_pass", "type": "prompt", "position": { "x": 280, "y": -90 }, "data": { "label": "Audience research", "sourceType": "raw", "source": "", "rawPrompt": "You are researching the AUDIENCE angle of a marketing strategy.\\n\\nProduct brief:\\n{{ brief }}\\n\\nWho specifically is this for, what are they trying to get done, and what are they doing instead today?\\n\\nSearch for whatever helps, including the product itself - researching a competitor or a named product is a normal use of this. Finding little about it is also a normal result: say so and work from the brief alone.\\n\\nEvery capability, channel, metric or price you state must come from the brief or from a source you found. If you have neither, say that plainly. Never fill the gap by inferring what a product like this usually does - a plausible category is not evidence.\\n\\nMark anything you inferred rather than found. Be concrete and brief - at most 250 words." } },
|
|
30930
|
+
{ "id": "messaging_pass", "type": "prompt", "position": { "x": 280, "y": 0 }, "data": { "label": "Messaging research", "sourceType": "raw", "source": "", "rawPrompt": "You are researching the MESSAGING angle of a marketing strategy.\\n\\nProduct brief:\\n{{ brief }}\\n\\nWhat do these people already believe, and what specific claim would move them? Say what the product is NOT for.\\n\\nSearch for whatever helps, including the product itself - researching a competitor or a named product is a normal use of this. Finding little about it is also a normal result: say so and work from the brief alone.\\n\\nEvery capability, channel, metric or price you state must come from the brief or from a source you found. If you have neither, say that plainly. Never fill the gap by inferring what a product like this usually does - a plausible category is not evidence.\\n\\nMark anything you inferred rather than found. Be concrete and brief - at most 250 words." } },
|
|
30931
|
+
{ "id": "channel_pass", "type": "prompt", "position": { "x": 280, "y": 90 }, "data": { "label": "Channel research", "sourceType": "raw", "source": "", "rawPrompt": "You are researching the CHANNEL angle of a marketing strategy.\\n\\nProduct brief:\\n{{ brief }}\\n\\nWhere does this audience already look for answers, and what format earns their attention there?\\n\\nSearch for whatever helps, including the product itself - researching a competitor or a named product is a normal use of this. Finding little about it is also a normal result: say so and work from the brief alone.\\n\\nEvery capability, channel, metric or price you state must come from the brief or from a source you found. If you have neither, say that plainly. Never fill the gap by inferring what a product like this usually does - a plausible category is not evidence.\\n\\nMark anything you inferred rather than found. Be concrete and brief - at most 250 words." } }
|
|
30932
|
+
],
|
|
30933
|
+
"edges": [
|
|
30934
|
+
{ "id": "e-start-angles", "source": "start", "target": "angles" },
|
|
30935
|
+
{ "id": "e-angles-audience_pass-f0", "source": "angles", "target": "audience_pass", "sourceHandle": "fork-0", "label": "Audience" },
|
|
30936
|
+
{ "id": "e-audience_pass-collect-f0", "source": "audience_pass", "target": "collect", "targetHandle": "input-0" },
|
|
30937
|
+
{ "id": "e-angles-messaging_pass-f1", "source": "angles", "target": "messaging_pass", "sourceHandle": "fork-1", "label": "Messaging" },
|
|
30938
|
+
{ "id": "e-messaging_pass-collect-f1", "source": "messaging_pass", "target": "collect", "targetHandle": "input-1" },
|
|
30939
|
+
{ "id": "e-angles-channel_pass-f2", "source": "angles", "target": "channel_pass", "sourceHandle": "fork-2", "label": "Channel" },
|
|
30940
|
+
{ "id": "e-channel_pass-collect-f2", "source": "channel_pass", "target": "collect", "targetHandle": "input-2" },
|
|
30941
|
+
{ "id": "e-collect-strategy", "source": "collect", "target": "strategy" },
|
|
30942
|
+
{ "id": "e-strategy-out", "source": "strategy", "target": "out" }
|
|
30943
|
+
]
|
|
30944
|
+
}
|
|
30945
|
+
`,
|
|
30946
|
+
"workflows/research-pipeline/workflow.pdflow": '{\n "version": "1.0",\n "metadata": {\n "id": "research-pipeline",\n "name": "Research Pipeline",\n "description": "Breaks a question into sub-questions, researches each one, then synthesizes an answer that must cite its sources."\n },\n "parameters": [\n { "name": "question", "type": "string", "required": true, "multiline": true, "description": "What do you want researched? One question, as specific as you can make it." }\n ],\n "nodes": [\n { "id": "start", "type": "trigger", "position": { "x": 0, "y": 0 }, "data": { "label": "Start", "triggerType": "manual" } },\n { "id": "decompose", "type": "prompt", "position": { "x": 0, "y": 200 }, "data": { "label": "Break it down", "sourceType": "raw", "source": "", "rawPrompt": "Break this research question into between three and six sub-questions that together answer it. Each must be answerable on its own and worth answering separately.\\n\\nQuestion: {{ question }}\\n\\nReturn ONLY a JSON array of strings. No commentary, no code fence." } },\n { "id": "each", "type": "loop", "position": { "x": 0, "y": 300 }, "data": { "label": "Research each sub-question", "loopType": "for-each", "items": "{{ decompose }}", "arraySource": "{{ decompose }}", "itemVariable": "subq", "body": ["sub_pass"], "onComplete": "gather", "maxIterations": 6 } },\n { "id": "sub_pass", "type": "agent", "parentId": "each", "position": { "x": 260, "y": 300 }, "data": { "label": "Answer one sub-question", "systemPrompt": "You research one narrow question at a time. Search before answering, and search more than once - one query returns one neighbourhood of an argument. Attribute every load-bearing claim to a source with its date. Separate what the sources support from what you inferred. If the evidence is thin, say so; that is a finding.", "userPrompt": "Sub-question: {{ subq }}\\n\\nContext - the overall question being researched: {{ question }}\\n\\nAnswer in at most 250 words, with sources.", "maxIterations": 6, "outputMode": "final-response" } },\n { "id": "sub_tools", "type": "tool", "position": { "x": 520, "y": 300 }, "data": { "label": "Search", "attachedTo": "sub_pass", "tools": ["web_search"] } },\n { "id": "gather", "type": "transformer", "position": { "x": 0, "y": 420 }, "data": { "label": "Gather findings", "mode": "template", "template": "ORIGINAL QUESTION\\n{{ question }}\\n\\nSUB-QUESTIONS\\n{{ decompose }}\\n\\nFINDINGS\\n{{ each }}" } },\n { "id": "synthesize", "type": "prompt", "position": { "x": 0, "y": 520 }, "data": { "label": "Synthesize", "sourceType": "raw", "source": "", "rawPrompt": "Write the answer to the original question from the findings below.\\n\\n{{ input }}\\n\\nLead with the answer. Attribute every load-bearing claim to its source. Where findings disagree, report the disagreement rather than picking a winner silently. Mark inferences as inferences. End with how confident you are and what would change your mind." } },\n { "id": "sourced", "type": "guardrail", "position": { "x": 0, "y": 620 }, "data": { "label": "Sources present", "systemPrompt": "You are checking one thing: does the text below attribute its load-bearing factual claims to named sources? A synthesis that reads as confident but cites nothing FAILS. Inferences that are explicitly marked as inferences are acceptable and do not need a source.\\n\\nReply with JSON only: {\\"pass\\": true} or {\\"pass\\": false, \\"reason\\": \\"<one sentence>\\"}.", "passExpression": "{{ pass }}" } },\n { "id": "out", "type": "output", "position": { "x": 0, "y": 720 }, "data": { "label": "Answer" } }\n ],\n "edges": [\n { "id": "e2", "source": "start", "target": "decompose" },\n { "id": "e3", "source": "decompose", "target": "each" },\n { "id": "e4", "source": "gather", "target": "synthesize" },\n { "id": "e5", "source": "synthesize", "target": "sourced" },\n { "id": "e6", "source": "sourced", "target": "out" }\n ]\n}\n',
|
|
30947
|
+
"workflows/rfp-questionnaire-response/workflow.pdflow": `{
|
|
30948
|
+
"version": "1.0",
|
|
30949
|
+
"metadata": {
|
|
30950
|
+
"id": "rfp-questionnaire-response",
|
|
30951
|
+
"name": "RFP / Questionnaire Response",
|
|
30952
|
+
"description": "Extracts every question from an RFP or vendor questionnaire, drafts each answer from your own documents, and refuses to claim anything the documents do not support."
|
|
30953
|
+
},
|
|
30954
|
+
"parameters": [
|
|
30955
|
+
{ "name": "doc", "type": "string", "required": true, "multiline": true, "description": "Paste the RFP or questionnaire text." }
|
|
30956
|
+
],
|
|
30957
|
+
"nodes": [
|
|
30958
|
+
{ "id": "start", "type": "trigger", "position": { "x": 0, "y": 0 }, "data": { "label": "Start", "triggerType": "manual" } },
|
|
30959
|
+
{ "id": "extract", "type": "prompt", "position": { "x": 0, "y": 200 }, "data": { "label": "Extract the questions", "sourceType": "raw", "source": "", "rawPrompt": "Extract every question that requires an answer from the document below. Keep each question's original wording. Do not merge, reorder or invent questions.\\n\\n{{ doc }}\\n\\nReturn ONLY a JSON array of strings. No commentary, no code fence." } },
|
|
30960
|
+
{ "id": "each", "type": "loop", "position": { "x": 0, "y": 300 }, "data": { "label": "Answer each question", "loopType": "for-each", "items": "{{ extract }}", "arraySource": "{{ extract }}", "itemVariable": "q", "body": ["answer"], "onComplete": "assemble", "maxIterations": 40 } },
|
|
30961
|
+
{ "id": "answer", "type": "agent", "parentId": "each", "position": { "x": 260, "y": 300 }, "data": { "label": "Draft one answer", "systemPrompt": "You draft answers to procurement and security questionnaires, grounded ONLY in the documents available to you in this workspace. Search the workspace before answering. Quote or reference the document that supports each claim. If you cannot find support for a capability, say plainly that it is not evidenced here and describe what would be needed to answer - never write a plausible answer to fill the gap. An unanswered question is recoverable; a false claim in a binding document is not.", "userPrompt": "Question: {{ q }}\\n\\nDraft the answer, then on a final line state either SUPPORTED (with the document you relied on) or UNSUPPORTED.", "maxIterations": 8, "outputMode": "final-response" } },
|
|
30962
|
+
{ "id": "answer_tools", "type": "tool", "position": { "x": 520, "y": 300 }, "data": { "label": "Read the workspace", "attachedTo": "answer", "tools": ["read_file", "list_files", "search_files", "stat_file"] } },
|
|
30963
|
+
{ "id": "assemble", "type": "transformer", "position": { "x": 0, "y": 420 }, "data": { "label": "Assemble", "mode": "template", "template": "DRAFT RESPONSE\\n\\n{{ each }}" } },
|
|
30964
|
+
{ "id": "no_overclaim", "type": "guardrail", "position": { "x": 0, "y": 520 }, "data": { "label": "No unsupported claims", "systemPrompt": "You are checking a draft questionnaire response for claims that its own evidence does not support. An answer marked UNSUPPORTED that honestly says so is FINE and must pass. An answer that asserts a capability, certification or control while showing no supporting document FAILS.\\n\\nReply with JSON only: {\\"pass\\": true} or {\\"pass\\": false, \\"reason\\": \\"<which answer and why>\\"}.", "passExpression": "{{ pass }}" } },
|
|
30965
|
+
{ "id": "final", "type": "prompt", "position": { "x": 0, "y": 620 }, "data": { "label": "Response and gap list", "sourceType": "raw", "source": "", "rawPrompt": "Produce two sections from the draft below.\\n\\n{{ input }}\\n\\nFirst: the response, question by question, using only the supported answers.\\nSecond: a gap list of every question marked UNSUPPORTED, each with what evidence would be needed to answer it. Do not soften the gap list - it is the part a human must act on." } },
|
|
30966
|
+
{ "id": "out", "type": "output", "position": { "x": 0, "y": 720 }, "data": { "label": "Response" } }
|
|
30967
|
+
],
|
|
30968
|
+
"edges": [
|
|
30969
|
+
{ "id": "e2", "source": "start", "target": "extract" },
|
|
30970
|
+
{ "id": "e3", "source": "extract", "target": "each" },
|
|
30971
|
+
{ "id": "e4", "source": "assemble", "target": "no_overclaim" },
|
|
30972
|
+
{ "id": "e5", "source": "no_overclaim", "target": "final" },
|
|
30973
|
+
{ "id": "e6", "source": "final", "target": "out" }
|
|
30974
|
+
]
|
|
30975
|
+
}
|
|
30976
|
+
`
|
|
30607
30977
|
};
|
|
30608
30978
|
|
|
30609
30979
|
// ../harness/prompts/library/index.ts
|
|
@@ -32437,6 +32807,8 @@ function makeCrewTurnRuntime(opts) {
|
|
|
32437
32807
|
policy,
|
|
32438
32808
|
permission,
|
|
32439
32809
|
...deps.maxTurns !== void 0 ? { maxTurns: deps.maxTurns } : {},
|
|
32810
|
+
...deps.compression ? { compression: deps.compression } : {},
|
|
32811
|
+
...deps.compaction ? { compaction: deps.compaction } : {},
|
|
32440
32812
|
...deps.temperature !== void 0 ? { temperature: deps.temperature } : {},
|
|
32441
32813
|
...deps.describePermissionDenial ? { describePermissionDenial: deps.describePermissionDenial } : {},
|
|
32442
32814
|
signal: mergeSignals(deps.toolCtx.signal, turnAbort.signal),
|
|
@@ -32452,7 +32824,7 @@ function makeCrewTurnRuntime(opts) {
|
|
|
32452
32824
|
deps.onEvent?.(e2);
|
|
32453
32825
|
}
|
|
32454
32826
|
});
|
|
32455
|
-
const produced = result.messages.slice(
|
|
32827
|
+
const produced = result.messages.slice(result.messages.length - result.appended);
|
|
32456
32828
|
const inputTokens = result.usage.promptTokens ?? 0;
|
|
32457
32829
|
const outputTokens = result.usage.completionTokens ?? 0;
|
|
32458
32830
|
const ceilingAborted = turnAbort.signal.aborted;
|
|
@@ -33325,7 +33697,7 @@ var LineSplitter = class {
|
|
|
33325
33697
|
init_fileService();
|
|
33326
33698
|
|
|
33327
33699
|
// src/session/msb.ts
|
|
33328
|
-
import { spawn as
|
|
33700
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
33329
33701
|
var DEFAULT_LIMITS = {
|
|
33330
33702
|
cpus: 1,
|
|
33331
33703
|
memory: "512M",
|
|
@@ -33372,7 +33744,7 @@ function wslHostPath(p2) {
|
|
|
33372
33744
|
function makeMsbStreamer(bin, wrap) {
|
|
33373
33745
|
return (args, onLine, opts) => {
|
|
33374
33746
|
const { cmd, argv } = wrap ? wrap(args) : { cmd: bin, argv: args };
|
|
33375
|
-
const child =
|
|
33747
|
+
const child = spawn4(cmd, argv, { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
33376
33748
|
const out = new LineSplitter();
|
|
33377
33749
|
const err = new LineSplitter();
|
|
33378
33750
|
let stderrText = "";
|
|
@@ -33423,7 +33795,7 @@ function wslMsb(bin) {
|
|
|
33423
33795
|
function makeMsbRunner(bin, wrap) {
|
|
33424
33796
|
return (args, opts) => new Promise((resolveRun) => {
|
|
33425
33797
|
const { cmd, argv } = wrap ? wrap(args) : { cmd: bin, argv: args };
|
|
33426
|
-
const child =
|
|
33798
|
+
const child = spawn4(cmd, argv, { windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
33427
33799
|
let stdout = "";
|
|
33428
33800
|
let stderr = "";
|
|
33429
33801
|
let settled = false;
|
|
@@ -34164,17 +34536,17 @@ function makeGuestRunner(deps) {
|
|
|
34164
34536
|
}
|
|
34165
34537
|
|
|
34166
34538
|
// src/session/sessionsPort.ts
|
|
34167
|
-
function toInfo(h2,
|
|
34539
|
+
function toInfo(h2, backend2) {
|
|
34168
34540
|
return {
|
|
34169
34541
|
id: h2.id,
|
|
34170
34542
|
status: h2.status,
|
|
34171
34543
|
workdir: h2.workdir,
|
|
34172
34544
|
createdAt: h2.createdAt,
|
|
34173
34545
|
...h2.owner ? { owner: h2.owner } : {},
|
|
34174
|
-
isolated:
|
|
34175
|
-
kind:
|
|
34546
|
+
isolated: backend2.isolation !== "none",
|
|
34547
|
+
kind: backend2.kind,
|
|
34176
34548
|
// Feature-detected once, here, rather than by a client calling and failing.
|
|
34177
|
-
canCapture: !!
|
|
34549
|
+
canCapture: !!backend2.commit,
|
|
34178
34550
|
// Read from the HANDLE, so it survives every later list() — and, on the
|
|
34179
34551
|
// microVM backend, a daemon restart, since the sandbox carries it in its
|
|
34180
34552
|
// own labels.
|
|
@@ -34185,9 +34557,9 @@ function toInfo(h2, backend) {
|
|
|
34185
34557
|
};
|
|
34186
34558
|
}
|
|
34187
34559
|
function makeSessionsPort(deps) {
|
|
34188
|
-
const { manager, backend } = deps;
|
|
34560
|
+
const { manager, backend: backend2 } = deps;
|
|
34189
34561
|
return {
|
|
34190
|
-
list: () => manager.list().map((h2) => toInfo(h2,
|
|
34562
|
+
list: () => manager.list().map((h2) => toInfo(h2, backend2)),
|
|
34191
34563
|
async create(spec) {
|
|
34192
34564
|
let source;
|
|
34193
34565
|
let workdir = spec.workdir;
|
|
@@ -34221,7 +34593,7 @@ function makeSessionsPort(deps) {
|
|
|
34221
34593
|
...spec.stateDir ? { stateDir: spec.stateDir } : {},
|
|
34222
34594
|
...env && Object.keys(env).length ? { env } : {}
|
|
34223
34595
|
});
|
|
34224
|
-
return { ...toInfo(h2,
|
|
34596
|
+
return { ...toInfo(h2, backend2), ...source ? { source } : {} };
|
|
34225
34597
|
},
|
|
34226
34598
|
canClone: deps.git?.enabled === true && deps.approveCheckout !== void 0,
|
|
34227
34599
|
/* Straight through to the manager, which is where admission and the
|
|
@@ -34239,9 +34611,9 @@ function makeSessionsPort(deps) {
|
|
|
34239
34611
|
* client feature-detects by asking rather than by calling and failing. A
|
|
34240
34612
|
* process-backed session genuinely cannot capture or stream, and saying so
|
|
34241
34613
|
* up front is better than an error at the moment someone tries. */
|
|
34242
|
-
...
|
|
34614
|
+
...backend2.commit ? {
|
|
34243
34615
|
commit: async (id, name) => {
|
|
34244
|
-
const snap = await
|
|
34616
|
+
const snap = await backend2.commit(id, name);
|
|
34245
34617
|
return {
|
|
34246
34618
|
name: snap.name,
|
|
34247
34619
|
createdAt: snap.createdAt,
|
|
@@ -34249,26 +34621,26 @@ function makeSessionsPort(deps) {
|
|
|
34249
34621
|
};
|
|
34250
34622
|
}
|
|
34251
34623
|
} : {},
|
|
34252
|
-
...
|
|
34253
|
-
snapshots: async () => (await
|
|
34624
|
+
...backend2.snapshots ? {
|
|
34625
|
+
snapshots: async () => (await backend2.snapshots()).map((s2) => ({
|
|
34254
34626
|
name: s2.name,
|
|
34255
34627
|
createdAt: s2.createdAt,
|
|
34256
34628
|
...s2.sessionId ? { sessionId: s2.sessionId } : {},
|
|
34257
34629
|
...s2.sizeBytes !== void 0 ? { sizeBytes: s2.sizeBytes } : {}
|
|
34258
34630
|
}))
|
|
34259
34631
|
} : {},
|
|
34260
|
-
...
|
|
34261
|
-
snapshotUsage: () =>
|
|
34632
|
+
...backend2.snapshotUsage ? {
|
|
34633
|
+
snapshotUsage: () => backend2.snapshotUsage()
|
|
34262
34634
|
} : {},
|
|
34263
|
-
...
|
|
34635
|
+
...backend2.metrics ? {
|
|
34264
34636
|
watchMetrics: (id, onSample) => {
|
|
34265
|
-
const st2 =
|
|
34637
|
+
const st2 = backend2.metrics(id, onSample);
|
|
34266
34638
|
return () => st2.stop();
|
|
34267
34639
|
}
|
|
34268
34640
|
} : {},
|
|
34269
|
-
...
|
|
34641
|
+
...backend2.logs ? {
|
|
34270
34642
|
watchLogs: (id, onLine) => {
|
|
34271
|
-
const st2 =
|
|
34643
|
+
const st2 = backend2.logs(id, onLine);
|
|
34272
34644
|
return () => st2.stop();
|
|
34273
34645
|
}
|
|
34274
34646
|
} : {}
|
|
@@ -34291,16 +34663,16 @@ function portExtras(opts) {
|
|
|
34291
34663
|
}
|
|
34292
34664
|
async function wireSessions(opts) {
|
|
34293
34665
|
if (opts.isolation === "none") {
|
|
34294
|
-
const
|
|
34666
|
+
const backend3 = makeProcessBackend({ roots: opts.roots, ...opts.log ? { log: opts.log } : {} });
|
|
34295
34667
|
opts.log?.("sessions: isolation none (runs execute in the daemon process)");
|
|
34296
34668
|
const manager2 = new SessionManager({
|
|
34297
|
-
backend:
|
|
34669
|
+
backend: backend3,
|
|
34298
34670
|
runner: opts.runner,
|
|
34299
34671
|
...opts.recordRun ? { recordRun: opts.recordRun } : {},
|
|
34300
34672
|
...opts.recordTranscript ? { recordTranscript: opts.recordTranscript } : {},
|
|
34301
34673
|
...opts.log ? { log: opts.log } : {}
|
|
34302
34674
|
});
|
|
34303
|
-
return { manager: manager2, backend:
|
|
34675
|
+
return { manager: manager2, backend: backend3, port: makeSessionsPort({ manager: manager2, backend: backend3, guestEnv, ...portExtras(opts) }) };
|
|
34304
34676
|
}
|
|
34305
34677
|
const viaWsl = process.platform === "win32";
|
|
34306
34678
|
const makeRunner = opts.makeRunner ?? ((bin) => makeMsbRunner(bin, viaWsl ? wslMsb(bin) : void 0));
|
|
@@ -34313,7 +34685,7 @@ async function wireSessions(opts) {
|
|
|
34313
34685
|
On Windows msb runs inside WSL2 \u2014 there is no native build \u2014 so the daemon must run there too.`
|
|
34314
34686
|
);
|
|
34315
34687
|
}
|
|
34316
|
-
const
|
|
34688
|
+
const backend2 = makeMicrovmBackend({
|
|
34317
34689
|
roots: opts.roots,
|
|
34318
34690
|
run,
|
|
34319
34691
|
stream: makeMsbStreamer(opts.msbBin, viaWsl ? wslMsb(opts.msbBin) : void 0),
|
|
@@ -34324,14 +34696,14 @@ async function wireSessions(opts) {
|
|
|
34324
34696
|
...opts.log ? { log: opts.log } : {}
|
|
34325
34697
|
});
|
|
34326
34698
|
const manager = new SessionManager({
|
|
34327
|
-
backend,
|
|
34699
|
+
backend: backend2,
|
|
34328
34700
|
runner: opts.runner,
|
|
34329
34701
|
...opts.recordRun ? { recordRun: opts.recordRun } : {},
|
|
34330
34702
|
...opts.recordTranscript ? { recordTranscript: opts.recordTranscript } : {},
|
|
34331
34703
|
// One runner per session, built on demand: the manager knows which session a
|
|
34332
34704
|
// run belongs to, so nothing has to infer it.
|
|
34333
34705
|
guestRunnerFor: (sessionId, guestOpts) => makeGuestRunner({
|
|
34334
|
-
backend,
|
|
34706
|
+
backend: backend2,
|
|
34335
34707
|
sessionId: () => sessionId,
|
|
34336
34708
|
allowMutations: guestOpts.allowMutations,
|
|
34337
34709
|
...opts.log ? { log: opts.log } : {}
|
|
@@ -34339,19 +34711,19 @@ async function wireSessions(opts) {
|
|
|
34339
34711
|
...opts.log ? { log: opts.log } : {}
|
|
34340
34712
|
});
|
|
34341
34713
|
opts.log?.(`sessions: isolation microvm via ${version} (image ${opts.image})`);
|
|
34342
|
-
await
|
|
34714
|
+
await backend2.adopt?.().catch((e2) => {
|
|
34343
34715
|
opts.log?.(`sessions: adoption failed (${e2 instanceof Error ? e2.message : String(e2)}) \u2014 starting with none adopted`);
|
|
34344
34716
|
});
|
|
34345
|
-
return { manager, backend, port: makeSessionsPort({ manager, backend, guestEnv, ...portExtras(opts) }) };
|
|
34717
|
+
return { manager, backend: backend2, port: makeSessionsPort({ manager, backend: backend2, guestEnv, ...portExtras(opts) }) };
|
|
34346
34718
|
}
|
|
34347
34719
|
|
|
34348
34720
|
// src/runner/crewSessions.ts
|
|
34349
34721
|
function makeCrewSessions(src2, opts) {
|
|
34350
34722
|
if (!src2?.manager.isolated) return void 0;
|
|
34351
|
-
const { manager, backend } = src2;
|
|
34723
|
+
const { manager, backend: backend2 } = src2;
|
|
34352
34724
|
return {
|
|
34353
|
-
list: () =>
|
|
34354
|
-
adopt: () =>
|
|
34725
|
+
list: () => backend2.list(),
|
|
34726
|
+
adopt: () => backend2.adopt?.() ?? Promise.resolve(void 0),
|
|
34355
34727
|
create: async (spec) => {
|
|
34356
34728
|
const env = await opts.guestEnv().catch(() => ({}));
|
|
34357
34729
|
return manager.create({
|
|
@@ -34367,15 +34739,15 @@ function makeCrewSessions(src2, opts) {
|
|
|
34367
34739
|
|
|
34368
34740
|
// src/runner/owners.ts
|
|
34369
34741
|
init_tools_node();
|
|
34370
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
34742
|
+
import { readdir, readFile as readFile2 } from "node:fs/promises";
|
|
34371
34743
|
import { createHash as createHash2 } from "node:crypto";
|
|
34372
|
-
import { join as
|
|
34744
|
+
import { join as join9, resolve as resolve6 } from "node:path";
|
|
34373
34745
|
init_protocol();
|
|
34374
34746
|
|
|
34375
34747
|
// src/hostsDir.ts
|
|
34376
34748
|
init_tools_node();
|
|
34377
34749
|
init_protocol();
|
|
34378
|
-
import { join as
|
|
34750
|
+
import { join as join8 } from "node:path";
|
|
34379
34751
|
var HOSTS_SEG = "hosts";
|
|
34380
34752
|
var MAX_SYNC_PATH_LEN = 400;
|
|
34381
34753
|
var MAX_SYNC_SEGMENTS = 24;
|
|
@@ -34397,11 +34769,11 @@ function assertSyncPath(path) {
|
|
|
34397
34769
|
}
|
|
34398
34770
|
}
|
|
34399
34771
|
function hostsRoot(stateDir) {
|
|
34400
|
-
return
|
|
34772
|
+
return join8(stateDir, "sidecar", HOSTS_SEG);
|
|
34401
34773
|
}
|
|
34402
34774
|
function hostsDirFor(stateDir, dirName2) {
|
|
34403
34775
|
if (!DIR_NAME_RE.test(dirName2)) throw new Error(`invalid host dir name: ${dirName2}`);
|
|
34404
|
-
return
|
|
34776
|
+
return join8(hostsRoot(stateDir), dirName2);
|
|
34405
34777
|
}
|
|
34406
34778
|
var RAW = /* @__PURE__ */ new WeakMap();
|
|
34407
34779
|
function hostFilesFor(stateDir, dirName2) {
|
|
@@ -34441,7 +34813,7 @@ var WORKSPACE_ID_RE = /^[a-z0-9_-]{1,32}$/;
|
|
|
34441
34813
|
async function workspaceRootsFor(dirName2, hostDir) {
|
|
34442
34814
|
let entries;
|
|
34443
34815
|
try {
|
|
34444
|
-
entries = await readdir(
|
|
34816
|
+
entries = await readdir(join9(hostDir, WORKSPACES_SEG), { withFileTypes: true });
|
|
34445
34817
|
} catch {
|
|
34446
34818
|
return [];
|
|
34447
34819
|
}
|
|
@@ -34449,7 +34821,7 @@ async function workspaceRootsFor(dirName2, hostDir) {
|
|
|
34449
34821
|
for (const entry of entries) {
|
|
34450
34822
|
if (!entry.isDirectory()) continue;
|
|
34451
34823
|
if (!WORKSPACE_ID_RE.test(entry.name)) continue;
|
|
34452
|
-
const wsDir =
|
|
34824
|
+
const wsDir = join9(hostDir, WORKSPACES_SEG, entry.name);
|
|
34453
34825
|
out.push({
|
|
34454
34826
|
owner: `${dirName2}/ws/${entry.name}`,
|
|
34455
34827
|
files: new NodeFileService(wsDir),
|
|
@@ -34462,7 +34834,7 @@ async function workspaceRootsFor(dirName2, hostDir) {
|
|
|
34462
34834
|
async function readCrewOptIn(root, dotDir = stateDirName()) {
|
|
34463
34835
|
let raw;
|
|
34464
34836
|
try {
|
|
34465
|
-
raw = await
|
|
34837
|
+
raw = await readFile2(join9(root, dotDir, CREW_OPT_IN_FILE), "utf8");
|
|
34466
34838
|
} catch {
|
|
34467
34839
|
return null;
|
|
34468
34840
|
}
|
|
@@ -34570,8 +34942,8 @@ async function listOwnerRoots(stateDir, workRoots = [], dotDir = stateDirName())
|
|
|
34570
34942
|
}
|
|
34571
34943
|
|
|
34572
34944
|
// src/crewDeployment.ts
|
|
34573
|
-
import { readFile as
|
|
34574
|
-
import { join as
|
|
34945
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
34946
|
+
import { join as join10 } from "node:path";
|
|
34575
34947
|
function splitOwner(owner) {
|
|
34576
34948
|
if (owner === "") return void 0;
|
|
34577
34949
|
const parts = owner.split("/");
|
|
@@ -34584,7 +34956,7 @@ async function deployedRoot(stateDir, owner, agent) {
|
|
|
34584
34956
|
if (!parts) return void 0;
|
|
34585
34957
|
let text;
|
|
34586
34958
|
try {
|
|
34587
|
-
text = await
|
|
34959
|
+
text = await readFile3(join10(hostsDirFor(stateDir, parts.dirName), DEPLOYMENT_FILE), "utf8");
|
|
34588
34960
|
} catch {
|
|
34589
34961
|
return void 0;
|
|
34590
34962
|
}
|
|
@@ -34597,7 +34969,7 @@ init_tools_node();
|
|
|
34597
34969
|
// src/cert.ts
|
|
34598
34970
|
import { promises as fs9 } from "node:fs";
|
|
34599
34971
|
import { networkInterfaces as networkInterfaces2 } from "node:os";
|
|
34600
|
-
import { join as
|
|
34972
|
+
import { join as join11 } from "node:path";
|
|
34601
34973
|
import selfsigned from "selfsigned";
|
|
34602
34974
|
function lanIPv4s() {
|
|
34603
34975
|
const out = [];
|
|
@@ -34608,9 +34980,9 @@ function lanIPv4s() {
|
|
|
34608
34980
|
}
|
|
34609
34981
|
return [...new Set(out)];
|
|
34610
34982
|
}
|
|
34611
|
-
var certPath = () =>
|
|
34612
|
-
var keyPath = () =>
|
|
34613
|
-
var sansPath = () =>
|
|
34983
|
+
var certPath = () => join11(sidecarSubdir(), "sidecar.crt");
|
|
34984
|
+
var keyPath = () => join11(sidecarSubdir(), "sidecar.key");
|
|
34985
|
+
var sansPath = () => join11(sidecarSubdir(), "sidecar.sans.json");
|
|
34614
34986
|
function selfSignedPaths() {
|
|
34615
34987
|
return { cert: certPath(), key: keyPath() };
|
|
34616
34988
|
}
|
|
@@ -34682,7 +35054,7 @@ function tunnelIds() {
|
|
|
34682
35054
|
}
|
|
34683
35055
|
|
|
34684
35056
|
// src/tunnel/zrok.ts
|
|
34685
|
-
import { spawn as
|
|
35057
|
+
import { spawn as spawn5 } from "node:child_process";
|
|
34686
35058
|
function extractShareUrl(s2) {
|
|
34687
35059
|
const full = s2.match(/https?:\/\/[A-Za-z0-9.-]+(?::\d+)?(?:\/[^\s"'\\]*)?/);
|
|
34688
35060
|
if (full) return full[0];
|
|
@@ -34728,7 +35100,7 @@ function stopChild(child) {
|
|
|
34728
35100
|
}
|
|
34729
35101
|
function reserveName(bin, name) {
|
|
34730
35102
|
return new Promise((res, rej) => {
|
|
34731
|
-
const child =
|
|
35103
|
+
const child = spawn5(bin, ["create", "name", name], { stdio: ["ignore", "ignore", "ignore"] });
|
|
34732
35104
|
let settled = false;
|
|
34733
35105
|
const done = (fn) => {
|
|
34734
35106
|
if (settled) return;
|
|
@@ -34750,7 +35122,7 @@ function reserveName(bin, name) {
|
|
|
34750
35122
|
async function startWith(bin, port, name) {
|
|
34751
35123
|
if (name) await reserveName(bin, name);
|
|
34752
35124
|
return new Promise((resolveHandle, reject) => {
|
|
34753
|
-
const child =
|
|
35125
|
+
const child = spawn5(bin, shareArgs(port, name), { stdio: ["ignore", "pipe", "pipe"] });
|
|
34754
35126
|
let out = "";
|
|
34755
35127
|
let settled = false;
|
|
34756
35128
|
const finish = (fn) => {
|
|
@@ -34799,7 +35171,7 @@ var zrokProvider = {
|
|
|
34799
35171
|
};
|
|
34800
35172
|
|
|
34801
35173
|
// src/tunnel/cloudflared.ts
|
|
34802
|
-
import { spawn as
|
|
35174
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
34803
35175
|
function extractTunnelUrl(s2) {
|
|
34804
35176
|
const m2 = s2.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
|
|
34805
35177
|
return m2 ? m2[0] : null;
|
|
@@ -34853,7 +35225,7 @@ function stopChild2(child) {
|
|
|
34853
35225
|
}
|
|
34854
35226
|
function startWith2(bin, mode, log, onDown) {
|
|
34855
35227
|
return new Promise((resolveHandle, reject) => {
|
|
34856
|
-
const child =
|
|
35228
|
+
const child = spawn6(bin, mode.argv, { stdio: ["ignore", "pipe", "pipe"] });
|
|
34857
35229
|
let out = "";
|
|
34858
35230
|
let settled = false;
|
|
34859
35231
|
let tailBuf = "";
|
|
@@ -35022,7 +35394,7 @@ async function superviseTunnel(opts) {
|
|
|
35022
35394
|
// src/runner/runnerService.ts
|
|
35023
35395
|
init_protocol();
|
|
35024
35396
|
init_tools_node();
|
|
35025
|
-
import { join as
|
|
35397
|
+
import { join as join12 } from "node:path";
|
|
35026
35398
|
|
|
35027
35399
|
// src/runner/location.ts
|
|
35028
35400
|
init_tools_node();
|
|
@@ -41781,7 +42153,7 @@ function nodeGatewayClient(endpoint, stateDir) {
|
|
|
41781
42153
|
}
|
|
41782
42154
|
|
|
41783
42155
|
// src/runner/executor.ts
|
|
41784
|
-
import { mkdir } from "node:fs/promises";
|
|
42156
|
+
import { mkdir as mkdir2 } from "node:fs/promises";
|
|
41785
42157
|
|
|
41786
42158
|
// ../harness/core/recovery.ts
|
|
41787
42159
|
var DEFAULT_RECOVERY = {
|
|
@@ -41995,6 +42367,93 @@ function formatOffloadRef(ref) {
|
|
|
41995
42367
|
${ref.preview}${ref.bytes > ref.preview.length ? "\n\u2026(truncated)" : ""}` : head;
|
|
41996
42368
|
}
|
|
41997
42369
|
|
|
42370
|
+
// ../harness/core/compactHistory.ts
|
|
42371
|
+
var CHECKPOINT_MARKER = "[checkpoint] ";
|
|
42372
|
+
var ANCHOR_MARKER = "[anchor] ";
|
|
42373
|
+
var COMPACT_AT_FILL = 0.86;
|
|
42374
|
+
function shouldCompact(fill, messages) {
|
|
42375
|
+
if (fill < COMPACT_AT_FILL) return false;
|
|
42376
|
+
return messages.some((m2) => m2.role !== "system" && m2.role !== "user");
|
|
42377
|
+
}
|
|
42378
|
+
var REARM_BELOW_FILL = 0.6;
|
|
42379
|
+
var REARM_ON_REGROWTH = 0.1;
|
|
42380
|
+
function afterCompaction() {
|
|
42381
|
+
return { armed: false, floor: null };
|
|
42382
|
+
}
|
|
42383
|
+
function messageChars(m2) {
|
|
42384
|
+
let n2 = 0;
|
|
42385
|
+
const content = m2.content;
|
|
42386
|
+
if (typeof content === "string") n2 += content.length;
|
|
42387
|
+
else if (Array.isArray(content)) {
|
|
42388
|
+
for (const part of content) {
|
|
42389
|
+
const text = part.text;
|
|
42390
|
+
if (typeof text === "string") n2 += text.length;
|
|
42391
|
+
}
|
|
42392
|
+
}
|
|
42393
|
+
const calls = m2.tool_calls;
|
|
42394
|
+
if (Array.isArray(calls)) {
|
|
42395
|
+
for (const c2 of calls) n2 += (c2.function?.name?.length ?? 0) + (c2.function?.arguments?.length ?? 0);
|
|
42396
|
+
}
|
|
42397
|
+
return n2;
|
|
42398
|
+
}
|
|
42399
|
+
function estimateFill(messages, contextWindow) {
|
|
42400
|
+
if (!contextWindow || contextWindow <= 0) return 0;
|
|
42401
|
+
let chars = 0;
|
|
42402
|
+
for (const m2 of messages) chars += messageChars(m2);
|
|
42403
|
+
return Math.min(1, chars / 4 / contextWindow);
|
|
42404
|
+
}
|
|
42405
|
+
function compactionDecision(fill, messages, gate) {
|
|
42406
|
+
if (!gate.armed) {
|
|
42407
|
+
const drained = fill < REARM_BELOW_FILL;
|
|
42408
|
+
const regrown = gate.floor !== null && fill >= gate.floor + REARM_ON_REGROWTH;
|
|
42409
|
+
return { compact: false, next: { ...gate, armed: drained || regrown } };
|
|
42410
|
+
}
|
|
42411
|
+
if (!shouldCompact(fill, messages)) return { compact: false, next: gate };
|
|
42412
|
+
return { compact: true, next: afterCompaction() };
|
|
42413
|
+
}
|
|
42414
|
+
async function compactHistory(messages, opts) {
|
|
42415
|
+
const tail3 = messages.length ? messages[messages.length - 1] : void 0;
|
|
42416
|
+
const pending = tail3 && isUnansweredToolCall(tail3) ? tail3 : null;
|
|
42417
|
+
const body2 = pending ? messages.slice(0, -1) : messages;
|
|
42418
|
+
const system = body2.filter((m2) => m2.role === "system");
|
|
42419
|
+
const collapsed = body2.filter((m2) => m2.role !== "system" && m2.role !== "user");
|
|
42420
|
+
const kept = [];
|
|
42421
|
+
let answered = null;
|
|
42422
|
+
for (const m2 of body2) {
|
|
42423
|
+
if (isOwnMarker(m2)) continue;
|
|
42424
|
+
if (m2.role === "assistant" && typeof m2.content === "string") answered = m2;
|
|
42425
|
+
if (m2.role !== "user") continue;
|
|
42426
|
+
if (opts.anchor) {
|
|
42427
|
+
kept.push({ role: "assistant", content: `${ANCHOR_MARKER}${await opts.anchor(m2, answered)}` });
|
|
42428
|
+
}
|
|
42429
|
+
kept.push(await offloadIfOversized(m2, opts));
|
|
42430
|
+
}
|
|
42431
|
+
const summary = await opts.summarize(collapsed);
|
|
42432
|
+
return {
|
|
42433
|
+
messages: [
|
|
42434
|
+
...system,
|
|
42435
|
+
{ role: "assistant", content: `${CHECKPOINT_MARKER}${summary}` },
|
|
42436
|
+
...kept,
|
|
42437
|
+
...pending ? [pending] : []
|
|
42438
|
+
],
|
|
42439
|
+
checkpoint: summary
|
|
42440
|
+
};
|
|
42441
|
+
}
|
|
42442
|
+
async function offloadIfOversized(m2, opts) {
|
|
42443
|
+
const limit2 = opts.offloadOverBytes;
|
|
42444
|
+
if (!opts.offload || limit2 === void 0) return m2;
|
|
42445
|
+
if (typeof m2.content !== "string" || m2.content.length <= limit2) return m2;
|
|
42446
|
+
return { ...m2, content: await opts.offload(m2.content) };
|
|
42447
|
+
}
|
|
42448
|
+
var isCompactionMarker = isOwnMarker;
|
|
42449
|
+
function isOwnMarker(m2) {
|
|
42450
|
+
if (m2.role !== "assistant" || typeof m2.content !== "string") return false;
|
|
42451
|
+
return m2.content.startsWith(ANCHOR_MARKER) || m2.content.startsWith(CHECKPOINT_MARKER);
|
|
42452
|
+
}
|
|
42453
|
+
function isUnansweredToolCall(m2) {
|
|
42454
|
+
return m2.role === "assistant" && Array.isArray(m2.tool_calls) && m2.tool_calls.length > 0;
|
|
42455
|
+
}
|
|
42456
|
+
|
|
41998
42457
|
// ../harness/prompts/toolsMarker.ts
|
|
41999
42458
|
var TOOLS_MARKER = "\uE000PROMPD_TOOLS\uE000";
|
|
42000
42459
|
function formatToolLines(tools) {
|
|
@@ -42115,6 +42574,7 @@ async function runAgent(args) {
|
|
|
42115
42574
|
const ctxWindow = compression?.contextWindow;
|
|
42116
42575
|
const compressionStrategy = compression?.strategy ?? "eager";
|
|
42117
42576
|
let fill = 0;
|
|
42577
|
+
let compactGate = { armed: true, floor: null };
|
|
42118
42578
|
const deferredOffloads = [];
|
|
42119
42579
|
const maxTurns = policy.maxTurns ?? args.maxTurns ?? 8;
|
|
42120
42580
|
const signal = args.signal ?? new AbortController().signal;
|
|
@@ -42132,6 +42592,16 @@ async function runAgent(args) {
|
|
|
42132
42592
|
turnAc?.abort();
|
|
42133
42593
|
});
|
|
42134
42594
|
const messages = [...args.messages];
|
|
42595
|
+
const incoming = new Set(args.messages);
|
|
42596
|
+
const appendedFromTail = () => {
|
|
42597
|
+
let n2 = 0;
|
|
42598
|
+
for (let i2 = messages.length - 1; i2 >= 0; i2--) {
|
|
42599
|
+
const m2 = messages[i2];
|
|
42600
|
+
if (incoming.has(m2) || m2.role === "system" || isCompactionMarker(m2)) break;
|
|
42601
|
+
n2++;
|
|
42602
|
+
}
|
|
42603
|
+
return n2;
|
|
42604
|
+
};
|
|
42135
42605
|
const deferred = args.deferredTools ?? [];
|
|
42136
42606
|
const loader = deferred.length ? createToolLoader({ deferred, loaded: args.loadedTools, onChange: (names) => emit({ type: "tools-loaded", names }) }) : null;
|
|
42137
42607
|
const eager = loader ? [...tools, loader.searchTool] : tools;
|
|
@@ -42173,6 +42643,31 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42173
42643
|
emit({ type: "injected", content: typeof m2.content === "string" ? m2.content : "" });
|
|
42174
42644
|
}
|
|
42175
42645
|
}
|
|
42646
|
+
const gateFill = fill || estimateFill(messages, ctxWindow);
|
|
42647
|
+
const gate = compactionDecision(gateFill, messages, compactGate);
|
|
42648
|
+
compactGate = gate.next;
|
|
42649
|
+
if (args.compaction && gate.compact) {
|
|
42650
|
+
const before = messages.length;
|
|
42651
|
+
try {
|
|
42652
|
+
const out = await compactHistory(messages, {
|
|
42653
|
+
summarize: args.compaction.summarize,
|
|
42654
|
+
anchor: args.compaction.anchor,
|
|
42655
|
+
offloadOverBytes: compression && compression.mode !== "off" ? effectiveThreshold(compression.mode, gateFill) : void 0,
|
|
42656
|
+
offload: toolCtx.recall ? async (content) => formatOffloadRef(await toolCtx.recall.record("user", content)) : void 0
|
|
42657
|
+
});
|
|
42658
|
+
messages.splice(0, messages.length, ...out.messages);
|
|
42659
|
+
deferredOffloads.length = 0;
|
|
42660
|
+
try {
|
|
42661
|
+
await args.compaction.onCheckpoint?.(out.checkpoint);
|
|
42662
|
+
} catch (e2) {
|
|
42663
|
+
alog("checkpoint persist failed", e2 instanceof Error ? e2.message : String(e2));
|
|
42664
|
+
}
|
|
42665
|
+
emit({ type: "checkpoint", collapsed: before - messages.length, checkpoint: out.checkpoint });
|
|
42666
|
+
} catch (e2) {
|
|
42667
|
+
compactGate = { ...compactGate, armed: true };
|
|
42668
|
+
alog("compaction failed", e2 instanceof Error ? e2.message : String(e2));
|
|
42669
|
+
}
|
|
42670
|
+
}
|
|
42176
42671
|
gstate.turn = turn - 1;
|
|
42177
42672
|
const turnGate = evaluateTurnStart(policy, gstate);
|
|
42178
42673
|
if (!turnGate.ok) {
|
|
@@ -42267,6 +42762,7 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42267
42762
|
usage.completionTokens += streamed.usage.completionTokens;
|
|
42268
42763
|
if (ctxWindow && ctxWindow > 0 && streamed.usage.promptTokens) {
|
|
42269
42764
|
fill = Math.min(1, streamed.usage.promptTokens / ctxWindow);
|
|
42765
|
+
if (!compactGate.armed && compactGate.floor === null) compactGate = { armed: false, floor: fill };
|
|
42270
42766
|
}
|
|
42271
42767
|
emit({ type: "usage", usage: streamed.usage });
|
|
42272
42768
|
const assistantMsg = streamed.toolCalls.length ? {
|
|
@@ -42315,13 +42811,13 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42315
42811
|
}
|
|
42316
42812
|
loader?.touch(tool.name);
|
|
42317
42813
|
const address = toolAddress(tool);
|
|
42318
|
-
const
|
|
42319
|
-
if (!
|
|
42320
|
-
const msg = `Blocked by policy "${policy.name}": ${
|
|
42814
|
+
const gate2 = evaluateToolCall(policy, gstate, { name: call.name, exists: true, address });
|
|
42815
|
+
if (!gate2.ok) {
|
|
42816
|
+
const msg = `Blocked by policy "${policy.name}": ${gate2.violation.message}`;
|
|
42321
42817
|
messages.push({ role: "tool", tool_call_id: call.id, content: `Error: ${msg}` });
|
|
42322
|
-
emit({ type: "policy-violation", kind:
|
|
42818
|
+
emit({ type: "policy-violation", kind: gate2.violation.kind, message: gate2.violation.message, action: gate2.action, tool: call.name });
|
|
42323
42819
|
emit({ type: "tool-result", id: call.id, name: call.name, result: msg, isError: true });
|
|
42324
|
-
if (
|
|
42820
|
+
if (gate2.action === "stop") {
|
|
42325
42821
|
policyStop = true;
|
|
42326
42822
|
break;
|
|
42327
42823
|
}
|
|
@@ -42404,7 +42900,7 @@ ${serverIndex(deferred)}` : serverIndex(deferred) : system;
|
|
|
42404
42900
|
}
|
|
42405
42901
|
alog("done", stopReason, "tokens", usage);
|
|
42406
42902
|
emit({ type: "done", reason: stopReason });
|
|
42407
|
-
return { messages, text, usage, stopReason };
|
|
42903
|
+
return { messages, text, usage, stopReason, appended: appendedFromTail() };
|
|
42408
42904
|
}
|
|
42409
42905
|
async function compactIncomingOffloads(messages, toolByName, compression, recall, emit) {
|
|
42410
42906
|
const nameForCallId = /* @__PURE__ */ new Map();
|
|
@@ -42490,8 +42986,29 @@ var JsonlSessionStore = class _JsonlSessionStore {
|
|
|
42490
42986
|
if (!f2) throw new Error("No file workspace is connected \u2014 sessions are unavailable.");
|
|
42491
42987
|
return f2;
|
|
42492
42988
|
}
|
|
42989
|
+
/* Sessions that have been MOVED out from under this store's root, id -> the
|
|
42990
|
+
* root they now live in.
|
|
42991
|
+
*
|
|
42992
|
+
* A session can move mid-run: start_project offers to bring the conversation
|
|
42993
|
+
* into the project it just created, and the run that asked is still writing.
|
|
42994
|
+
* Without this, the next save() would resolve the OLD path and recreate the
|
|
42995
|
+
* file there from its cursor -- the same conversation in two places, neither
|
|
42996
|
+
* complete. The cursor itself survives the move untouched: it is keyed by id
|
|
42997
|
+
* and holds the text, not the path.
|
|
42998
|
+
*
|
|
42999
|
+
* In memory only, and deliberately: it describes where a session went during
|
|
43000
|
+
* THIS page's life. A reload lists both roots afresh and needs no memory of
|
|
43001
|
+
* the journey. */
|
|
43002
|
+
movedRoots = /* @__PURE__ */ new Map();
|
|
43003
|
+
/** Resolve this id somewhere other than the store's own root, from now on. */
|
|
43004
|
+
setRootFor(id, root) {
|
|
43005
|
+
this.movedRoots.set(id, root);
|
|
43006
|
+
}
|
|
43007
|
+
rootFor(id) {
|
|
43008
|
+
return this.movedRoots.get(id) ?? this.root();
|
|
43009
|
+
}
|
|
42493
43010
|
path(id) {
|
|
42494
|
-
return `${this.
|
|
43011
|
+
return `${this.rootFor(id)}/${id}/SESSION.jsonl`;
|
|
42495
43012
|
}
|
|
42496
43013
|
async create(title, surface) {
|
|
42497
43014
|
const createdAt = now();
|
|
@@ -42660,10 +43177,108 @@ var JsonlSessionStore = class _JsonlSessionStore {
|
|
|
42660
43177
|
}
|
|
42661
43178
|
async delete(id) {
|
|
42662
43179
|
this.cursors.delete(id);
|
|
42663
|
-
await (await this.files()).remove(`${this.
|
|
43180
|
+
await (await this.files()).remove(`${this.rootFor(id)}/${id}`).catch(() => {
|
|
43181
|
+
});
|
|
43182
|
+
}
|
|
43183
|
+
};
|
|
43184
|
+
|
|
43185
|
+
// ../harness/sessions/checkpointStore.ts
|
|
43186
|
+
var CHECKPOINT_FILE = "CHECKPOINT.md";
|
|
43187
|
+
function makeCheckpointStore(getFiles, root) {
|
|
43188
|
+
return {
|
|
43189
|
+
async append(sessionId, body2) {
|
|
43190
|
+
const files = await getFiles();
|
|
43191
|
+
if (!files) return;
|
|
43192
|
+
const path = `${root}/${sessionId}/${CHECKPOINT_FILE}`;
|
|
43193
|
+
const entry = `## ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
43194
|
+
|
|
43195
|
+
${body2}
|
|
43196
|
+
`;
|
|
43197
|
+
let existing = null;
|
|
43198
|
+
try {
|
|
43199
|
+
existing = await files.read(path);
|
|
43200
|
+
} catch {
|
|
43201
|
+
existing = null;
|
|
43202
|
+
}
|
|
43203
|
+
if (existing === null) {
|
|
43204
|
+
await files.create(path, `# Checkpoints
|
|
43205
|
+
|
|
43206
|
+
${entry}`);
|
|
43207
|
+
return;
|
|
43208
|
+
}
|
|
43209
|
+
await files.write(path, `${existing}
|
|
43210
|
+
${entry}`);
|
|
43211
|
+
}
|
|
43212
|
+
};
|
|
43213
|
+
}
|
|
43214
|
+
|
|
43215
|
+
// ../harness/core/summarize.ts
|
|
43216
|
+
var TRUNCATION_NOTE = " \u2026[truncated]";
|
|
43217
|
+
var MIN_KEEP = 200;
|
|
43218
|
+
var SUMMARY_INPUT_CHARS = 6e4;
|
|
43219
|
+
var SYSTEM = [
|
|
43220
|
+
"You are compacting an agent conversation that is about to lose its middle.",
|
|
43221
|
+
"Write a dense record of what was DECIDED and what the current state is:",
|
|
43222
|
+
"conclusions reached, constraints the user set, files and artifacts touched,",
|
|
43223
|
+
"and anything still open. Omit narration, pleasantries and tool mechanics.",
|
|
43224
|
+
"Write plain prose or short bullets. No preamble."
|
|
43225
|
+
].join(" ");
|
|
43226
|
+
function makeSummarizer(client, model, opts = {}) {
|
|
43227
|
+
const maxChars = opts.maxChars ?? SUMMARY_INPUT_CHARS;
|
|
43228
|
+
return async (collapsed) => {
|
|
43229
|
+
const rendered = renderTail(collapsed, maxChars);
|
|
43230
|
+
const res = await client.chat.completions.create({
|
|
43231
|
+
model,
|
|
43232
|
+
messages: [
|
|
43233
|
+
{ role: "system", content: SYSTEM },
|
|
43234
|
+
{ role: "user", content: rendered }
|
|
43235
|
+
],
|
|
43236
|
+
stream: false
|
|
42664
43237
|
});
|
|
43238
|
+
return res.choices[0]?.message?.content ?? "";
|
|
43239
|
+
};
|
|
43240
|
+
}
|
|
43241
|
+
function renderTail(messages, maxChars) {
|
|
43242
|
+
const lines = [];
|
|
43243
|
+
let used = 0;
|
|
43244
|
+
for (let i2 = messages.length - 1; i2 >= 0; i2--) {
|
|
43245
|
+
const m2 = messages[i2];
|
|
43246
|
+
const text = typeof m2.content === "string" ? m2.content : JSON.stringify(m2.content ?? "");
|
|
43247
|
+
const line = `${m2.role}: ${text}`;
|
|
43248
|
+
const room = maxChars - used;
|
|
43249
|
+
if (line.length > room) {
|
|
43250
|
+
if (room > MIN_KEEP) lines.unshift(line.slice(0, room - TRUNCATION_NOTE.length) + TRUNCATION_NOTE);
|
|
43251
|
+
break;
|
|
43252
|
+
}
|
|
43253
|
+
lines.unshift(line);
|
|
43254
|
+
used += line.length;
|
|
42665
43255
|
}
|
|
43256
|
+
return lines.join("\n\n");
|
|
43257
|
+
}
|
|
43258
|
+
|
|
43259
|
+
// ../harness/providers/contextWindow.ts
|
|
43260
|
+
var KNOWN_WINDOWS = {
|
|
43261
|
+
"claude-sonnet-4-5": 2e5,
|
|
43262
|
+
"claude-haiku-4-5": 2e5,
|
|
43263
|
+
"claude-opus-4-1": 2e5,
|
|
43264
|
+
"gpt-4o-mini": 128e3,
|
|
43265
|
+
"gpt-4.1-mini": 1e6,
|
|
43266
|
+
"gpt-5": 4e5,
|
|
43267
|
+
"gpt-5-mini": 4e5,
|
|
43268
|
+
"gemini-2-5-pro": 1e6,
|
|
43269
|
+
"gemini-2-5-flash": 1e6
|
|
42666
43270
|
};
|
|
43271
|
+
var CONSERVATIVE_FALLBACK_WINDOW = 32e3;
|
|
43272
|
+
function stripDateSuffix(id) {
|
|
43273
|
+
return id.replace(/-\d{4}-\d{2}-\d{2}$/, "").replace(/-\d{8}$/, "");
|
|
43274
|
+
}
|
|
43275
|
+
function resolveContextWindow(modelId) {
|
|
43276
|
+
const id = modelId.trim().toLowerCase();
|
|
43277
|
+
if (id in KNOWN_WINDOWS) return { window: KNOWN_WINDOWS[id], source: "known" };
|
|
43278
|
+
const stripped = stripDateSuffix(id);
|
|
43279
|
+
if (stripped in KNOWN_WINDOWS) return { window: KNOWN_WINDOWS[stripped], source: "known" };
|
|
43280
|
+
return { window: CONSERVATIVE_FALLBACK_WINDOW, source: "fallback" };
|
|
43281
|
+
}
|
|
42667
43282
|
|
|
42668
43283
|
// src/runner/runLog.ts
|
|
42669
43284
|
function runLogPath(crewRoot2, agent) {
|
|
@@ -42792,7 +43407,7 @@ async function runCrewAgentNode(def, input, ctx, deps) {
|
|
|
42792
43407
|
const signal = ctx.signal ?? new AbortController().signal;
|
|
42793
43408
|
const resolveDef = crewResolveDef(ctx.stateFiles, ctx.stateRoot);
|
|
42794
43409
|
const kernelTools = crewToolSet(def, resolveDef)();
|
|
42795
|
-
await
|
|
43410
|
+
await mkdir2(ctx.memoryDir, { recursive: true });
|
|
42796
43411
|
const extraTools = deps.nodeTools([...ctx.workRoots, ctx.memoryDir]);
|
|
42797
43412
|
const tools = [...kernelTools, ...extraTools];
|
|
42798
43413
|
const conditionalOffers = [];
|
|
@@ -42894,6 +43509,21 @@ async function runCrewAgentNode(def, input, ctx, deps) {
|
|
|
42894
43509
|
// different answer. Supplied only when no prompt is wired; with one, a real
|
|
42895
43510
|
// person really did decide, and the default wording is correct.
|
|
42896
43511
|
...promptImpl ? {} : { describePermissionDenial: unattendedDenialMessage },
|
|
43512
|
+
/* `mode: 'off'` with a real `contextWindow` reads oddly but is exactly what
|
|
43513
|
+
* is wanted here: rung-1 offload stays off (this executor supplies no
|
|
43514
|
+
* `toolCtx.recall`, so there is nowhere to offload TO), while the loop still
|
|
43515
|
+
* computes `fill` — which is the only thing that lets it notice a window
|
|
43516
|
+
* about to overflow. Without a window, `fill` is pinned at 0 and compaction
|
|
43517
|
+
* below is dead code however well it is wired. */
|
|
43518
|
+
compression: { mode: "off", contextWindow: resolveContextWindow(modelId).window },
|
|
43519
|
+
/* Why the daemon in particular: these runs are unattended and long, so
|
|
43520
|
+
* nobody is watching to notice the wall and restart. Before this, hitting
|
|
43521
|
+
* it produced a provider 400 — fatal in recovery.ts, never retried — and
|
|
43522
|
+
* the run simply died mid-task. */
|
|
43523
|
+
compaction: {
|
|
43524
|
+
summarize: makeSummarizer(ctx.client, modelId),
|
|
43525
|
+
onCheckpoint: (body2) => makeCheckpointStore(() => ctx.stateFiles, sessionsRoot2).append(session.id, body2)
|
|
43526
|
+
},
|
|
42897
43527
|
signal: runSignal,
|
|
42898
43528
|
onEvent
|
|
42899
43529
|
});
|
|
@@ -43541,7 +44171,7 @@ var NodeRunnerService = class {
|
|
|
43541
44171
|
workFiles,
|
|
43542
44172
|
workRoots: [workDir],
|
|
43543
44173
|
client: this.client,
|
|
43544
|
-
memoryDir:
|
|
44174
|
+
memoryDir: join12(stateOwnerDir, stateRoot, agent, "memory"),
|
|
43545
44175
|
onEvent: trackedOnEvent,
|
|
43546
44176
|
sessionId,
|
|
43547
44177
|
signal: controller.signal,
|
|
@@ -43578,7 +44208,7 @@ var NodeRunnerService = class {
|
|
|
43578
44208
|
// src/runner/core.ts
|
|
43579
44209
|
import { watch } from "node:fs";
|
|
43580
44210
|
import { readdir as readdir2, stat } from "node:fs/promises";
|
|
43581
|
-
import { join as
|
|
44211
|
+
import { join as join14, relative as relative6, sep as sep4 } from "node:path";
|
|
43582
44212
|
|
|
43583
44213
|
// src/runner/triggerConfig.ts
|
|
43584
44214
|
function inputFor(trigger, fallback) {
|
|
@@ -44531,8 +45161,8 @@ async function resumeOpenRooms(opts) {
|
|
|
44531
45161
|
}
|
|
44532
45162
|
|
|
44533
45163
|
// src/runner/roomDeps.ts
|
|
44534
|
-
import { mkdir as
|
|
44535
|
-
import { join as
|
|
45164
|
+
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
45165
|
+
import { join as join13 } from "node:path";
|
|
44536
45166
|
|
|
44537
45167
|
// src/runner/roomAgentLookup.ts
|
|
44538
45168
|
init_tools_node();
|
|
@@ -44632,11 +45262,11 @@ async function makeDaemonRoomDeps(ctx, room, deps = {}) {
|
|
|
44632
45262
|
`cannot locate the memory folder for crew agent "${def.name}": this daemon has no state root for owner "${ctx.owner}"`
|
|
44633
45263
|
);
|
|
44634
45264
|
}
|
|
44635
|
-
return
|
|
45265
|
+
return join13(stateDir, agentMemoryRoot(ctx.stateRoot, def.name));
|
|
44636
45266
|
}
|
|
44637
45267
|
async function buildRuntime(def, p2, state, findings) {
|
|
44638
45268
|
const memoryDir = memoryDirFor(def);
|
|
44639
|
-
await
|
|
45269
|
+
await mkdir3(memoryDir, { recursive: true });
|
|
44640
45270
|
const toolCtx = {
|
|
44641
45271
|
signal: new AbortController().signal,
|
|
44642
45272
|
memory: makeAgentMemoryProvider(() => ctx.stateFiles, agentMemoryRoot(ctx.stateRoot, def.name)),
|
|
@@ -44686,7 +45316,19 @@ async function makeDaemonRoomDeps(ctx, room, deps = {}) {
|
|
|
44686
45316
|
// person really did decide and the default wording is correct.
|
|
44687
45317
|
...ctx.prompt ? {} : { describePermissionDenial: unattendedDenialMessage },
|
|
44688
45318
|
...ctx.now ? { now: ctx.now } : {},
|
|
44689
|
-
...ceilingUsd !== void 0 ? { turnCeilingUsd: ceilingUsd } : {}
|
|
45319
|
+
...ceilingUsd !== void 0 ? { turnCeilingUsd: ceilingUsd } : {},
|
|
45320
|
+
/* A room is the likeliest place in the product to meet the context
|
|
45321
|
+
* wall: every participant's turn is seeded with the WHOLE transcript,
|
|
45322
|
+
* tool calls included, so cost grows with participants x turns. Before
|
|
45323
|
+
* this, a room that filled the window died on a provider 400 -- fatal
|
|
45324
|
+
* in recovery.ts -- and took the turn with it.
|
|
45325
|
+
*
|
|
45326
|
+
* `mode: 'off'` with a real window is the same trade `executor.ts`
|
|
45327
|
+
* makes: rung-1 offload stays off (this toolCtx supplies no `recall`,
|
|
45328
|
+
* so there is nowhere to offload to) while the loop still computes
|
|
45329
|
+
* `fill`, which is the only thing that lets compaction fire. */
|
|
45330
|
+
compression: { mode: "off", contextWindow: resolveContextWindow(modelId).window },
|
|
45331
|
+
compaction: { summarize: makeSummarizer(ctx.client, modelId) }
|
|
44690
45332
|
}
|
|
44691
45333
|
});
|
|
44692
45334
|
}
|
|
@@ -44932,9 +45574,9 @@ function startDaemonCore(opts) {
|
|
|
44932
45574
|
for (const entry of entries) {
|
|
44933
45575
|
if (entry.isDirectory()) {
|
|
44934
45576
|
if (isExcludedSegment(entry.name, dotDir)) continue;
|
|
44935
|
-
await walkForChanges(root,
|
|
45577
|
+
await walkForChanges(root, join14(dir, entry.name), since);
|
|
44936
45578
|
} else {
|
|
44937
|
-
const abs =
|
|
45579
|
+
const abs = join14(dir, entry.name);
|
|
44938
45580
|
let mtimeMs;
|
|
44939
45581
|
try {
|
|
44940
45582
|
mtimeMs = (await stat(abs)).mtimeMs;
|
|
@@ -45246,9 +45888,9 @@ function makeRunHub(opts = {}) {
|
|
|
45246
45888
|
import { promises as fs11 } from "node:fs";
|
|
45247
45889
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
45248
45890
|
import { hostname } from "node:os";
|
|
45249
|
-
import { join as
|
|
45891
|
+
import { join as join15 } from "node:path";
|
|
45250
45892
|
function idPath(dir = sidecarSubdir()) {
|
|
45251
|
-
return
|
|
45893
|
+
return join15(dir, "daemon.id");
|
|
45252
45894
|
}
|
|
45253
45895
|
async function loadOrCreateSidecarId(dir = sidecarSubdir()) {
|
|
45254
45896
|
const path = idPath(dir);
|
|
@@ -45277,7 +45919,7 @@ function sidecarName(opts) {
|
|
|
45277
45919
|
// src/hosts.ts
|
|
45278
45920
|
import { promises as fs12 } from "node:fs";
|
|
45279
45921
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
45280
|
-
import { join as
|
|
45922
|
+
import { join as join16, dirname as dirname6 } from "node:path";
|
|
45281
45923
|
var MAX_DIR = 32;
|
|
45282
45924
|
var MAX_FIELD_LEN = 200;
|
|
45283
45925
|
var MAX_HOSTS = 200;
|
|
@@ -45311,7 +45953,7 @@ function sanitizeDirName(raw) {
|
|
|
45311
45953
|
return RESERVED_WINDOWS_NAMES.has(safe) ? `${safe}-host` : safe;
|
|
45312
45954
|
}
|
|
45313
45955
|
function hostsPath(stateDir) {
|
|
45314
|
-
return
|
|
45956
|
+
return join16(stateDir, "sidecar", "hosts.json");
|
|
45315
45957
|
}
|
|
45316
45958
|
function emptyManifest2() {
|
|
45317
45959
|
return /* @__PURE__ */ Object.create(null);
|
|
@@ -45351,10 +45993,10 @@ async function read(dir, fallbackLastSeen) {
|
|
|
45351
45993
|
return manifest;
|
|
45352
45994
|
}
|
|
45353
45995
|
async function write(stateDir, manifest) {
|
|
45354
|
-
const dir =
|
|
45996
|
+
const dir = dirname6(hostsPath(stateDir));
|
|
45355
45997
|
await fs12.mkdir(dir, { recursive: true });
|
|
45356
45998
|
const target = hostsPath(stateDir);
|
|
45357
|
-
const tmp =
|
|
45999
|
+
const tmp = join16(dir, `.hosts.json.tmp-${randomBytes3(8).toString("hex")}`);
|
|
45358
46000
|
try {
|
|
45359
46001
|
await fs12.writeFile(tmp, JSON.stringify(manifest, null, 2));
|
|
45360
46002
|
await fs12.rename(tmp, target);
|
|
@@ -45461,7 +46103,7 @@ function makeHostsManifest(dir) {
|
|
|
45461
46103
|
|
|
45462
46104
|
// src/hostsAdmin.ts
|
|
45463
46105
|
import { readdir as readdir3 } from "node:fs/promises";
|
|
45464
|
-
import { join as
|
|
46106
|
+
import { join as join17 } from "node:path";
|
|
45465
46107
|
|
|
45466
46108
|
// src/hostsCli.ts
|
|
45467
46109
|
init_tools_node();
|
|
@@ -45577,16 +46219,16 @@ async function countCrew(crewDir) {
|
|
|
45577
46219
|
return entries.filter((e2) => e2.isFile() && /\.(prmd|md)$/i.test(e2.name)).length;
|
|
45578
46220
|
}
|
|
45579
46221
|
async function countAgents(hostDir) {
|
|
45580
|
-
let total = await countCrew(
|
|
46222
|
+
let total = await countCrew(join17(hostDir, "crew"));
|
|
45581
46223
|
let workspaces;
|
|
45582
46224
|
try {
|
|
45583
|
-
workspaces = await readdir3(
|
|
46225
|
+
workspaces = await readdir3(join17(hostDir, WORKSPACES_SEG), { withFileTypes: true });
|
|
45584
46226
|
} catch {
|
|
45585
46227
|
return total;
|
|
45586
46228
|
}
|
|
45587
46229
|
for (const entry of workspaces) {
|
|
45588
46230
|
if (!entry.isDirectory()) continue;
|
|
45589
|
-
total += await countCrew(
|
|
46231
|
+
total += await countCrew(join17(hostDir, WORKSPACES_SEG, entry.name, "crew"));
|
|
45590
46232
|
}
|
|
45591
46233
|
return total;
|
|
45592
46234
|
}
|
|
@@ -45712,11 +46354,11 @@ function startAccountWatch(opts) {
|
|
|
45712
46354
|
// src/runner/pauseStore.ts
|
|
45713
46355
|
import { promises as fs14 } from "node:fs";
|
|
45714
46356
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
45715
|
-
import { join as
|
|
46357
|
+
import { join as join18, dirname as dirname7 } from "node:path";
|
|
45716
46358
|
var MAX_PAUSES = 1e3;
|
|
45717
46359
|
var MAX_FIELD_LEN2 = 200;
|
|
45718
46360
|
function pausesPath(stateDir) {
|
|
45719
|
-
return
|
|
46361
|
+
return join18(stateDir, "sidecar", "pauses.json");
|
|
45720
46362
|
}
|
|
45721
46363
|
function keyOf2(owner, agent) {
|
|
45722
46364
|
return `${owner}\0${agent}`;
|
|
@@ -45759,9 +46401,9 @@ async function read2(stateDir, fallbackPausedAt) {
|
|
|
45759
46401
|
}
|
|
45760
46402
|
async function write2(stateDir, paused) {
|
|
45761
46403
|
const target = pausesPath(stateDir);
|
|
45762
|
-
const dir =
|
|
46404
|
+
const dir = dirname7(target);
|
|
45763
46405
|
await fs14.mkdir(dir, { recursive: true });
|
|
45764
|
-
const tmp =
|
|
46406
|
+
const tmp = join18(dir, `.pauses.json.tmp-${randomBytes4(8).toString("hex")}`);
|
|
45765
46407
|
const body2 = { version: 1, paused: [...paused] };
|
|
45766
46408
|
try {
|
|
45767
46409
|
await fs14.writeFile(tmp, JSON.stringify(body2, null, 2));
|
|
@@ -46031,10 +46673,10 @@ async function runSessionsCli(argv, deps) {
|
|
|
46031
46673
|
}
|
|
46032
46674
|
if (verb === "create") {
|
|
46033
46675
|
const root = rootFrom(argv, process.cwd());
|
|
46034
|
-
const
|
|
46035
|
-
if (
|
|
46676
|
+
const backend2 = deps.backend ?? backendFor(deps, [root], run);
|
|
46677
|
+
if (backend2.adopt) await backend2.adopt().catch(() => void 0);
|
|
46036
46678
|
try {
|
|
46037
|
-
const handle = await
|
|
46679
|
+
const handle = await backend2.create({ workdir: root, owner: process.env.USERNAME || process.env.USER || "cli" });
|
|
46038
46680
|
out(`${handle.id}
|
|
46039
46681
|
`);
|
|
46040
46682
|
err(`created ${handle.id} on ${handle.workdir}
|
|
@@ -46063,19 +46705,19 @@ async function runSessionsCli(argv, deps) {
|
|
|
46063
46705
|
`);
|
|
46064
46706
|
return 1;
|
|
46065
46707
|
}
|
|
46066
|
-
const
|
|
46067
|
-
if (!
|
|
46708
|
+
const backend2 = deps.backend ?? backendFor(deps, [found.workdir], run);
|
|
46709
|
+
if (!backend2.exec) {
|
|
46068
46710
|
err("corenel sessions run: this backend cannot execute inside a session\n");
|
|
46069
46711
|
return 1;
|
|
46070
46712
|
}
|
|
46071
|
-
if (
|
|
46713
|
+
if (backend2.adopt) await backend2.adopt().catch(() => void 0);
|
|
46072
46714
|
try {
|
|
46073
46715
|
const token = await readAuthToken() ?? process.env.CORENEL_TOKEN ?? null;
|
|
46074
46716
|
if (!token) {
|
|
46075
46717
|
err("corenel sessions run: not signed in on this host.\n Run `corenel login` first \u2014 the session uses the host's credential.\n");
|
|
46076
46718
|
return 1;
|
|
46077
46719
|
}
|
|
46078
|
-
const r2 = await
|
|
46720
|
+
const r2 = await backend2.exec(found.id, ["corenel", "run", prompt], { env: { CORENEL_TOKEN: token } });
|
|
46079
46721
|
if (r2.stdout) out(r2.stdout.endsWith("\n") ? r2.stdout : `${r2.stdout}
|
|
46080
46722
|
`);
|
|
46081
46723
|
if (r2.stderr.trim()) err(r2.stderr.endsWith("\n") ? r2.stderr : `${r2.stderr}
|
|
@@ -46100,9 +46742,9 @@ async function runSessionsCli(argv, deps) {
|
|
|
46100
46742
|
`);
|
|
46101
46743
|
return 1;
|
|
46102
46744
|
}
|
|
46103
|
-
const
|
|
46104
|
-
if (
|
|
46105
|
-
const known = !!
|
|
46745
|
+
const backend2 = deps.backend ?? backendFor(deps, [found.workdir], run);
|
|
46746
|
+
if (backend2.adopt) await backend2.adopt().catch(() => void 0);
|
|
46747
|
+
const known = !!backend2.get?.(found.id);
|
|
46106
46748
|
if (!known) {
|
|
46107
46749
|
const r2 = await sandboxLifecycle(run, verb, found.sandbox);
|
|
46108
46750
|
if (!r2.ok) {
|
|
@@ -46124,7 +46766,7 @@ async function runSessionsCli(argv, deps) {
|
|
|
46124
46766
|
}
|
|
46125
46767
|
try {
|
|
46126
46768
|
if (verb === "pause") {
|
|
46127
|
-
const r2 = await
|
|
46769
|
+
const r2 = await backend2.pause(found.id);
|
|
46128
46770
|
if (r2 && !r2.drained) {
|
|
46129
46771
|
err(`paused ${found.id}, but ${r2.remaining.length} call(s) were STILL RUNNING when the drain gave up
|
|
46130
46772
|
`);
|
|
@@ -46135,12 +46777,12 @@ async function runSessionsCli(argv, deps) {
|
|
|
46135
46777
|
return 0;
|
|
46136
46778
|
}
|
|
46137
46779
|
if (verb === "resume") {
|
|
46138
|
-
await
|
|
46780
|
+
await backend2.resume(found.id);
|
|
46139
46781
|
out(`resumed ${found.id}
|
|
46140
46782
|
`);
|
|
46141
46783
|
return 0;
|
|
46142
46784
|
}
|
|
46143
|
-
await
|
|
46785
|
+
await backend2.destroy(found.id);
|
|
46144
46786
|
const still = (await listSessions(run)).find((r2) => r2.id === found.id);
|
|
46145
46787
|
if (still) {
|
|
46146
46788
|
err(`corenel sessions destroy: ${found.id} is still present (status ${still.status})
|
|
@@ -46564,7 +47206,7 @@ var FLAG_TO_KEY = {
|
|
|
46564
47206
|
"--default-model": "defaultModel",
|
|
46565
47207
|
"--name": "name"
|
|
46566
47208
|
};
|
|
46567
|
-
var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.8.
|
|
47209
|
+
var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.8.8";
|
|
46568
47210
|
function parseArgsWithProvenance(argv) {
|
|
46569
47211
|
const explicit = /* @__PURE__ */ new Set();
|
|
46570
47212
|
for (const a2 of argv) {
|
|
@@ -46706,78 +47348,6 @@ async function resolveEndpoint(args, env = process.env) {
|
|
|
46706
47348
|
});
|
|
46707
47349
|
}
|
|
46708
47350
|
|
|
46709
|
-
// src/config/daemonConfig.ts
|
|
46710
|
-
import { readFile as readFile3, writeFile, mkdir as mkdir3 } from "node:fs/promises";
|
|
46711
|
-
import { dirname as dirname7, join as join18 } from "node:path";
|
|
46712
|
-
var DAEMON_CONFIG_VERSION = 1;
|
|
46713
|
-
var SECRET_KEYS = ["acpAuth", "tunnelToken", "token"];
|
|
46714
|
-
var KNOWN_KEYS = [
|
|
46715
|
-
"version",
|
|
46716
|
-
"port",
|
|
46717
|
-
"host",
|
|
46718
|
-
"roots",
|
|
46719
|
-
"addUnder",
|
|
46720
|
-
"cloneUnder",
|
|
46721
|
-
"allowLan",
|
|
46722
|
-
"allowOrigins",
|
|
46723
|
-
"tlsCert",
|
|
46724
|
-
"tlsKey",
|
|
46725
|
-
"selfSigned",
|
|
46726
|
-
"tunnel",
|
|
46727
|
-
"tunnelName",
|
|
46728
|
-
"tunnelHostname",
|
|
46729
|
-
"endpoint",
|
|
46730
|
-
"allowShell",
|
|
46731
|
-
"keepTranscripts",
|
|
46732
|
-
"isolation",
|
|
46733
|
-
"msbBin",
|
|
46734
|
-
"guestImage",
|
|
46735
|
-
"maxChainDepth",
|
|
46736
|
-
"name",
|
|
46737
|
-
"appUrl",
|
|
46738
|
-
"defaultModel",
|
|
46739
|
-
"mcpServers",
|
|
46740
|
-
"mcpAllow"
|
|
46741
|
-
];
|
|
46742
|
-
function daemonConfigPath(stateDir) {
|
|
46743
|
-
return join18(stateDir, "daemon.json");
|
|
46744
|
-
}
|
|
46745
|
-
async function readDaemonConfig(stateDir) {
|
|
46746
|
-
const empty = (corrupt) => ({ config: { version: DAEMON_CONFIG_VERSION }, unknown: {}, corrupt });
|
|
46747
|
-
let raw;
|
|
46748
|
-
try {
|
|
46749
|
-
raw = await readFile3(daemonConfigPath(stateDir), "utf8");
|
|
46750
|
-
} catch {
|
|
46751
|
-
return empty(false);
|
|
46752
|
-
}
|
|
46753
|
-
let parsed;
|
|
46754
|
-
try {
|
|
46755
|
-
parsed = JSON.parse(raw);
|
|
46756
|
-
} catch {
|
|
46757
|
-
return empty(true);
|
|
46758
|
-
}
|
|
46759
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return empty(true);
|
|
46760
|
-
const src2 = parsed;
|
|
46761
|
-
const config = { version: DAEMON_CONFIG_VERSION };
|
|
46762
|
-
const unknown = {};
|
|
46763
|
-
for (const [k2, v2] of Object.entries(src2)) {
|
|
46764
|
-
if (k2 === "version") continue;
|
|
46765
|
-
if (KNOWN_KEYS.includes(k2)) {
|
|
46766
|
-
config[k2] = v2;
|
|
46767
|
-
} else {
|
|
46768
|
-
unknown[k2] = v2;
|
|
46769
|
-
}
|
|
46770
|
-
}
|
|
46771
|
-
return { config, unknown, corrupt: false };
|
|
46772
|
-
}
|
|
46773
|
-
async function writeDaemonConfig(stateDir, next, unknown = {}) {
|
|
46774
|
-
const body2 = { ...unknown, ...next, version: DAEMON_CONFIG_VERSION };
|
|
46775
|
-
const path = daemonConfigPath(stateDir);
|
|
46776
|
-
await mkdir3(dirname7(path), { recursive: true });
|
|
46777
|
-
await writeFile(path, `${JSON.stringify(body2, null, 2)}
|
|
46778
|
-
`, "utf8");
|
|
46779
|
-
}
|
|
46780
|
-
|
|
46781
47351
|
// src/config/layer.ts
|
|
46782
47352
|
var STRICT_BOOLEAN_KEYS = /* @__PURE__ */ new Set(["allowShell", "allowLan", "selfSigned", "keepTranscripts"]);
|
|
46783
47353
|
var KEY_MAP = {
|
|
@@ -47204,7 +47774,162 @@ async function runConfig(raw, io) {
|
|
|
47204
47774
|
return 2;
|
|
47205
47775
|
}
|
|
47206
47776
|
|
|
47207
|
-
// src/
|
|
47777
|
+
// ../mcp/src/httpClient.ts
|
|
47778
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
47779
|
+
var CLIENT_INFO = { name: "corenel", version: "0.2.0" };
|
|
47780
|
+
var SESSION_HEADER = "Mcp-Session-Id";
|
|
47781
|
+
var McpHttpClient = class {
|
|
47782
|
+
constructor(server, auth, fetchImpl = fetch) {
|
|
47783
|
+
this.server = server;
|
|
47784
|
+
this.auth = auth;
|
|
47785
|
+
this.fetchImpl = fetchImpl;
|
|
47786
|
+
}
|
|
47787
|
+
nextRequestId = 1;
|
|
47788
|
+
sessionId;
|
|
47789
|
+
async initialize() {
|
|
47790
|
+
await this.send("initialize", {
|
|
47791
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
47792
|
+
capabilities: {},
|
|
47793
|
+
clientInfo: CLIENT_INFO
|
|
47794
|
+
});
|
|
47795
|
+
}
|
|
47796
|
+
async listTools() {
|
|
47797
|
+
const result = await this.send("tools/list", {});
|
|
47798
|
+
const tools = isRecord2(result) ? result.tools : void 0;
|
|
47799
|
+
if (!Array.isArray(tools)) return [];
|
|
47800
|
+
return tools.filter(isMcpTool);
|
|
47801
|
+
}
|
|
47802
|
+
async callTool(name, args) {
|
|
47803
|
+
const result = await this.send("tools/call", { name, arguments: args });
|
|
47804
|
+
const record = isRecord2(result) ? result : {};
|
|
47805
|
+
return { content: record.content, isError: record.isError === true };
|
|
47806
|
+
}
|
|
47807
|
+
/** Send one JSON-RPC request, retrying exactly once on a 401 that the
|
|
47808
|
+
* AuthProvider says it recovered from. `isRetry` prevents a second retry
|
|
47809
|
+
* loop -- a credential that fails twice is refused, not flaky. */
|
|
47810
|
+
async send(method, params, isRetry = false) {
|
|
47811
|
+
const headers = await this.buildHeaders();
|
|
47812
|
+
const id = this.nextRequestId++;
|
|
47813
|
+
const res = await this.request(headers, id, method, params);
|
|
47814
|
+
if (res.status === 401) {
|
|
47815
|
+
if (!isRetry && await this.auth.onUnauthorized()) {
|
|
47816
|
+
return this.send(method, params, true);
|
|
47817
|
+
}
|
|
47818
|
+
throw new Error(
|
|
47819
|
+
`MCP server "${this.server.name}" refused the credential (401) -- run: corenel mcp login ${this.server.name}`
|
|
47820
|
+
);
|
|
47821
|
+
}
|
|
47822
|
+
const sessionId = res.headers.get(SESSION_HEADER);
|
|
47823
|
+
if (sessionId) this.sessionId = sessionId;
|
|
47824
|
+
if (!res.ok) {
|
|
47825
|
+
throw new Error(`MCP server "${this.server.name}" returned HTTP ${res.status}`);
|
|
47826
|
+
}
|
|
47827
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
47828
|
+
let body2;
|
|
47829
|
+
if (contentType.includes("text/event-stream")) {
|
|
47830
|
+
body2 = await this.readSseResponse(res, id);
|
|
47831
|
+
} else if (contentType.includes("application/json")) {
|
|
47832
|
+
body2 = await res.json();
|
|
47833
|
+
} else {
|
|
47834
|
+
throw new Error(
|
|
47835
|
+
`MCP server "${this.server.name}" answered with an unexpected content-type: "${contentType || "(none)"}"`
|
|
47836
|
+
);
|
|
47837
|
+
}
|
|
47838
|
+
if (body2.error) {
|
|
47839
|
+
throw new Error(`MCP server "${this.server.name}" returned error ${body2.error.code}: ${body2.error.message}`);
|
|
47840
|
+
}
|
|
47841
|
+
return body2.result;
|
|
47842
|
+
}
|
|
47843
|
+
/** Read an SSE body until the JSON-RPC response for `id` arrives.
|
|
47844
|
+
*
|
|
47845
|
+
* Events are separated by a blank line; an event's `data:` lines are joined
|
|
47846
|
+
* with newlines, per the SSE spec, and parsed as one JSON-RPC message.
|
|
47847
|
+
* Notifications (no `id`) and responses to other ids are skipped. The reader
|
|
47848
|
+
* is cancelled once the answer is in, so a server that keeps the stream open
|
|
47849
|
+
* after replying cannot hold this call open with it. */
|
|
47850
|
+
async readSseResponse(res, id) {
|
|
47851
|
+
if (!res.body) {
|
|
47852
|
+
throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
|
|
47853
|
+
}
|
|
47854
|
+
const reader = res.body.getReader();
|
|
47855
|
+
const decoder = new TextDecoder();
|
|
47856
|
+
let buffer = "";
|
|
47857
|
+
try {
|
|
47858
|
+
for (; ; ) {
|
|
47859
|
+
const { done, value } = await reader.read();
|
|
47860
|
+
if (value) buffer += decoder.decode(value, { stream: true });
|
|
47861
|
+
buffer = buffer.replace(/\r\n/g, "\n");
|
|
47862
|
+
let boundary = buffer.indexOf("\n\n");
|
|
47863
|
+
while (boundary !== -1) {
|
|
47864
|
+
const event = buffer.slice(0, boundary);
|
|
47865
|
+
buffer = buffer.slice(boundary + 2);
|
|
47866
|
+
const match = this.matchSseEvent(event, id);
|
|
47867
|
+
if (match) return match;
|
|
47868
|
+
boundary = buffer.indexOf("\n\n");
|
|
47869
|
+
}
|
|
47870
|
+
if (done) {
|
|
47871
|
+
const tail3 = buffer.trim() ? this.matchSseEvent(buffer, id) : null;
|
|
47872
|
+
if (tail3) return tail3;
|
|
47873
|
+
throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
|
|
47874
|
+
}
|
|
47875
|
+
}
|
|
47876
|
+
} finally {
|
|
47877
|
+
reader.cancel().catch(() => {
|
|
47878
|
+
});
|
|
47879
|
+
}
|
|
47880
|
+
}
|
|
47881
|
+
/** The JSON-RPC response in one SSE event, if it answers `id`; else null. */
|
|
47882
|
+
matchSseEvent(event, id) {
|
|
47883
|
+
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
47884
|
+
if (!data) return null;
|
|
47885
|
+
let parsed;
|
|
47886
|
+
try {
|
|
47887
|
+
parsed = JSON.parse(data);
|
|
47888
|
+
} catch {
|
|
47889
|
+
return null;
|
|
47890
|
+
}
|
|
47891
|
+
if (!isRecord2(parsed) || parsed.id !== id) return null;
|
|
47892
|
+
if (!("result" in parsed) && !("error" in parsed)) return null;
|
|
47893
|
+
return parsed;
|
|
47894
|
+
}
|
|
47895
|
+
/** The bare `fetch()` call, wrapped -- every OTHER throw in `send` already
|
|
47896
|
+
* names the server; this is the one call whose own failures (DNS miss,
|
|
47897
|
+
* connection refused, TLS failure) never reach `send`'s response-handling
|
|
47898
|
+
* at all. Left unguarded, node's bare `fetch failed` -- naming neither the
|
|
47899
|
+
* URL nor the actual cause -- propagates straight out of `send`, into
|
|
47900
|
+
* `registry.ts`'s `status().error`, and out of `doctor`, the one command
|
|
47901
|
+
* whose entire job is diagnosis. `err.cause` carries the real reason
|
|
47902
|
+
* (ECONNREFUSED, ENOTFOUND, a TLS alert); undici puts it there rather than
|
|
47903
|
+
* in `err.message`. */
|
|
47904
|
+
async request(headers, id, method, params) {
|
|
47905
|
+
try {
|
|
47906
|
+
return await this.fetchImpl(this.server.url, {
|
|
47907
|
+
method: "POST",
|
|
47908
|
+
headers,
|
|
47909
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
47910
|
+
});
|
|
47911
|
+
} catch (err) {
|
|
47912
|
+
const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : err instanceof Error ? err.message : String(err);
|
|
47913
|
+
throw new Error(`MCP server "${this.server.name}" (${this.server.url}) is unreachable: ${cause}`);
|
|
47914
|
+
}
|
|
47915
|
+
}
|
|
47916
|
+
async buildHeaders() {
|
|
47917
|
+
const headers = new Headers();
|
|
47918
|
+
headers.set("Content-Type", "application/json");
|
|
47919
|
+
headers.set("Accept", "application/json, text/event-stream");
|
|
47920
|
+
for (const h2 of await this.auth.headers()) headers.set(h2.name, h2.value);
|
|
47921
|
+
if (this.sessionId) headers.set(SESSION_HEADER, this.sessionId);
|
|
47922
|
+
return headers;
|
|
47923
|
+
}
|
|
47924
|
+
};
|
|
47925
|
+
function isRecord2(value) {
|
|
47926
|
+
return typeof value === "object" && value !== null;
|
|
47927
|
+
}
|
|
47928
|
+
function isMcpTool(value) {
|
|
47929
|
+
return isRecord2(value) && typeof value.name === "string" && "inputSchema" in value;
|
|
47930
|
+
}
|
|
47931
|
+
|
|
47932
|
+
// ../mcp/src/config.ts
|
|
47208
47933
|
init_tools_node();
|
|
47209
47934
|
import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir4, chmod } from "node:fs/promises";
|
|
47210
47935
|
function isUsableServer(v2) {
|
|
@@ -47323,7 +48048,7 @@ async function writeMcpCredential(stateDir, server, cred) {
|
|
|
47323
48048
|
}
|
|
47324
48049
|
}
|
|
47325
48050
|
|
|
47326
|
-
// src/
|
|
48051
|
+
// ../mcp/src/oauth/discovery.ts
|
|
47327
48052
|
var PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
|
47328
48053
|
var AUTH_SERVER_METADATA_PATH = "/.well-known/oauth-authorization-server";
|
|
47329
48054
|
async function tryFetchJson(url, fetchImpl) {
|
|
@@ -47421,7 +48146,423 @@ async function discoverAuthMetadata(authServer, fetchImpl = fetch) {
|
|
|
47421
48146
|
return { authorizationEndpoint, tokenEndpoint, registrationEndpoint, scopesSupported };
|
|
47422
48147
|
}
|
|
47423
48148
|
|
|
47424
|
-
// src/
|
|
48149
|
+
// ../mcp/src/oauth/tokens.ts
|
|
48150
|
+
function parseTokenResponse(body2) {
|
|
48151
|
+
if (typeof body2 !== "object" || body2 === null || Array.isArray(body2)) {
|
|
48152
|
+
throw new Error("token endpoint returned a malformed response");
|
|
48153
|
+
}
|
|
48154
|
+
const r2 = body2;
|
|
48155
|
+
if (typeof r2.access_token !== "string" || r2.access_token.length === 0) {
|
|
48156
|
+
throw new Error("token endpoint response did not include an access_token");
|
|
48157
|
+
}
|
|
48158
|
+
return {
|
|
48159
|
+
access_token: r2.access_token,
|
|
48160
|
+
...typeof r2.refresh_token === "string" ? { refresh_token: r2.refresh_token } : {},
|
|
48161
|
+
...typeof r2.expires_in === "number" ? { expires_in: r2.expires_in } : {},
|
|
48162
|
+
...typeof r2.scope === "string" ? { scope: r2.scope } : {}
|
|
48163
|
+
};
|
|
48164
|
+
}
|
|
48165
|
+
async function postToken(endpoint, body2, fetchImpl) {
|
|
48166
|
+
const res = await fetchImpl(endpoint, {
|
|
48167
|
+
method: "POST",
|
|
48168
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
48169
|
+
body: body2.toString()
|
|
48170
|
+
});
|
|
48171
|
+
if (!res.ok) {
|
|
48172
|
+
throw new Error(`token request failed at ${endpoint} (HTTP ${res.status})`);
|
|
48173
|
+
}
|
|
48174
|
+
return parseTokenResponse(await res.json());
|
|
48175
|
+
}
|
|
48176
|
+
async function exchangeCode(meta, opts, fetchImpl = fetch, now2 = Date.now) {
|
|
48177
|
+
const body2 = new URLSearchParams({
|
|
48178
|
+
grant_type: "authorization_code",
|
|
48179
|
+
client_id: opts.clientId,
|
|
48180
|
+
code: opts.code,
|
|
48181
|
+
code_verifier: opts.verifier,
|
|
48182
|
+
redirect_uri: opts.redirectUri,
|
|
48183
|
+
resource: opts.resource
|
|
48184
|
+
});
|
|
48185
|
+
if (opts.clientSecret) body2.set("client_secret", opts.clientSecret);
|
|
48186
|
+
const tokens = await postToken(meta.tokenEndpoint, body2, fetchImpl);
|
|
48187
|
+
return {
|
|
48188
|
+
kind: "oauth",
|
|
48189
|
+
accessToken: tokens.access_token,
|
|
48190
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {},
|
|
48191
|
+
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
48192
|
+
clientId: opts.clientId,
|
|
48193
|
+
...opts.clientSecret ? { clientSecret: opts.clientSecret } : {},
|
|
48194
|
+
authServer: opts.authServer,
|
|
48195
|
+
...tokens.scope ? { scope: tokens.scope } : {}
|
|
48196
|
+
};
|
|
48197
|
+
}
|
|
48198
|
+
async function refresh(meta, cred, fetchImpl = fetch, now2 = Date.now) {
|
|
48199
|
+
if (!cred.refreshToken) {
|
|
48200
|
+
throw new Error("credential has no refresh token to refresh with -- sign in again");
|
|
48201
|
+
}
|
|
48202
|
+
const body2 = new URLSearchParams({
|
|
48203
|
+
grant_type: "refresh_token",
|
|
48204
|
+
refresh_token: cred.refreshToken,
|
|
48205
|
+
client_id: cred.clientId
|
|
48206
|
+
});
|
|
48207
|
+
if (cred.clientSecret) body2.set("client_secret", cred.clientSecret);
|
|
48208
|
+
const tokens = await postToken(meta.tokenEndpoint, body2, fetchImpl);
|
|
48209
|
+
return {
|
|
48210
|
+
kind: "oauth",
|
|
48211
|
+
accessToken: tokens.access_token,
|
|
48212
|
+
// Keep the old refresh token when the server did not send a new one.
|
|
48213
|
+
refreshToken: tokens.refresh_token ?? cred.refreshToken,
|
|
48214
|
+
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
48215
|
+
clientId: cred.clientId,
|
|
48216
|
+
...cred.clientSecret ? { clientSecret: cred.clientSecret } : {},
|
|
48217
|
+
authServer: cred.authServer,
|
|
48218
|
+
scope: tokens.scope ?? cred.scope
|
|
48219
|
+
};
|
|
48220
|
+
}
|
|
48221
|
+
var EXPIRY_SKEW_MS = 6e4;
|
|
48222
|
+
function isExpired(cred, nowMs) {
|
|
48223
|
+
if (cred.expiresAt === void 0) return false;
|
|
48224
|
+
return cred.expiresAt - nowMs <= EXPIRY_SKEW_MS;
|
|
48225
|
+
}
|
|
48226
|
+
|
|
48227
|
+
// ../mcp/src/auth.ts
|
|
48228
|
+
function authProviderFor(stateDir, server, fetchImpl = fetch, accountToken = async () => null) {
|
|
48229
|
+
async function doRefresh(cred) {
|
|
48230
|
+
if (!cred.refreshToken) {
|
|
48231
|
+
throw new Error(`"${server.name}" has no refresh token on file -- run: corenel mcp login ${server.name}`);
|
|
48232
|
+
}
|
|
48233
|
+
const meta = await discoverAuthMetadata(cred.authServer, fetchImpl);
|
|
48234
|
+
const refreshed = await refresh(meta, cred, fetchImpl);
|
|
48235
|
+
await writeMcpCredential(stateDir, server.name, refreshed);
|
|
48236
|
+
return refreshed;
|
|
48237
|
+
}
|
|
48238
|
+
return {
|
|
48239
|
+
async headers() {
|
|
48240
|
+
const cred = await readMcpCredential(stateDir, server.name);
|
|
48241
|
+
if (!cred && !server.hosted) {
|
|
48242
|
+
throw new Error(`No credential stored for MCP server "${server.name}" -- run: corenel mcp login ${server.name} (or set-header, for a static token).`);
|
|
48243
|
+
}
|
|
48244
|
+
const mcpHeaders = !cred ? [] : cred.kind === "headers" ? cred.headers : [{ name: "Authorization", value: `Bearer ${(isExpired(cred, Date.now()) ? await doRefresh(cred) : cred).accessToken}` }];
|
|
48245
|
+
if (!server.hosted) return mcpHeaders;
|
|
48246
|
+
const moved = mcpHeaders.map((h2) => h2.name.toLowerCase() === "authorization" ? { name: "X-MCP-Authorization", value: h2.value } : h2);
|
|
48247
|
+
const account = await accountToken();
|
|
48248
|
+
return account ? [...moved, { name: "Authorization", value: `Bearer ${account}` }] : moved;
|
|
48249
|
+
},
|
|
48250
|
+
async onUnauthorized() {
|
|
48251
|
+
const cred = await readMcpCredential(stateDir, server.name);
|
|
48252
|
+
if (!cred || cred.kind !== "oauth") return false;
|
|
48253
|
+
try {
|
|
48254
|
+
await doRefresh(cred);
|
|
48255
|
+
return true;
|
|
48256
|
+
} catch {
|
|
48257
|
+
return false;
|
|
48258
|
+
}
|
|
48259
|
+
}
|
|
48260
|
+
};
|
|
48261
|
+
}
|
|
48262
|
+
|
|
48263
|
+
// ../mcp/src/timedFetch.ts
|
|
48264
|
+
var AUTH_REQUEST_TIMEOUT_MS = 15e3;
|
|
48265
|
+
function timedFetch(ms, inner = fetch) {
|
|
48266
|
+
return async (input, init2 = {}) => {
|
|
48267
|
+
const deadline = AbortSignal.timeout(ms);
|
|
48268
|
+
const signal = init2.signal ? AbortSignal.any([init2.signal, deadline]) : deadline;
|
|
48269
|
+
try {
|
|
48270
|
+
return await inner(input, { ...init2, signal });
|
|
48271
|
+
} catch (err) {
|
|
48272
|
+
if (deadline.aborted && !init2.signal?.aborted) {
|
|
48273
|
+
throw new Error(`no answer within ${ms / 1e3}s`, { cause: err });
|
|
48274
|
+
}
|
|
48275
|
+
throw err;
|
|
48276
|
+
}
|
|
48277
|
+
};
|
|
48278
|
+
}
|
|
48279
|
+
|
|
48280
|
+
// ../mcp/src/registry.ts
|
|
48281
|
+
async function accountTokenFor(stateDir) {
|
|
48282
|
+
try {
|
|
48283
|
+
const cred = await resolveCredential(process.env, stateDir);
|
|
48284
|
+
return cred.token ?? null;
|
|
48285
|
+
} catch {
|
|
48286
|
+
return null;
|
|
48287
|
+
}
|
|
48288
|
+
}
|
|
48289
|
+
function defaultClientFactory(stateDir, server) {
|
|
48290
|
+
return new McpHttpClient(
|
|
48291
|
+
server,
|
|
48292
|
+
authProviderFor(stateDir, server, timedFetch(AUTH_REQUEST_TIMEOUT_MS), () => accountTokenFor(stateDir))
|
|
48293
|
+
);
|
|
48294
|
+
}
|
|
48295
|
+
var SERVER_CONNECT_TIMEOUT_MS = 3e4;
|
|
48296
|
+
var McpRegistry = class {
|
|
48297
|
+
constructor(stateDir, clientFactory = defaultClientFactory) {
|
|
48298
|
+
this.stateDir = stateDir;
|
|
48299
|
+
this.clientFactory = clientFactory;
|
|
48300
|
+
}
|
|
48301
|
+
connected = /* @__PURE__ */ new Map();
|
|
48302
|
+
statuses = /* @__PURE__ */ new Map();
|
|
48303
|
+
// The order the operator wrote servers in daemon.json, captured once per
|
|
48304
|
+
// connectAll() -- `tools()` walks THIS, never `connected`'s Map iteration
|
|
48305
|
+
// order. `connectOne` runs concurrently for every server (Promise.all), so
|
|
48306
|
+
// Map insertion order is race order: whichever server's initialize+list
|
|
48307
|
+
// happened to resolve first. That is fine for `connected` (a lookup table,
|
|
48308
|
+
// order never read) but `tools()` is a list a model/doctor/policy consumes
|
|
48309
|
+
// in order, and race order means a different tool ordering on every daemon
|
|
48310
|
+
// boot -- exactly the kind of bug that surfaces far from here and is never
|
|
48311
|
+
// traced back. Configured order is also information the operator supplied
|
|
48312
|
+
// deliberately; sorting alphabetically would discard it.
|
|
48313
|
+
serverOrder = [];
|
|
48314
|
+
// Tool lists seeded from the on-disk cache, with no socket ever opened.
|
|
48315
|
+
// Deliberately a THIRD piece of state, not folded into `connected`: a
|
|
48316
|
+
// primed server can answer `tools()` but must still go through
|
|
48317
|
+
// `ensureConnected` before `call()` will dispatch to it. `connectAll`
|
|
48318
|
+
// does not touch this map -- see the comment at its first line for why.
|
|
48319
|
+
primed = /* @__PURE__ */ new Map();
|
|
48320
|
+
// One in-flight ensureConnected() attempt per server, so three tool calls
|
|
48321
|
+
// that land on the same cold server in the same turn share one connection
|
|
48322
|
+
// instead of racing three.
|
|
48323
|
+
connecting = /* @__PURE__ */ new Map();
|
|
48324
|
+
/**
|
|
48325
|
+
* Connect to every configured server and cache its tools. Never throws --
|
|
48326
|
+
* a server that fails to initialize, or fails listing tools, is recorded
|
|
48327
|
+
* in `status()` with zero tools and the daemon keeps starting.
|
|
48328
|
+
*/
|
|
48329
|
+
async connectAll(opts = {}) {
|
|
48330
|
+
this.connected.clear();
|
|
48331
|
+
this.statuses.clear();
|
|
48332
|
+
this.primed.clear();
|
|
48333
|
+
const servers = await readMcpServers(this.stateDir);
|
|
48334
|
+
this.serverOrder = servers.map((s2) => s2.name);
|
|
48335
|
+
const timeoutMs = opts.timeoutMs ?? SERVER_CONNECT_TIMEOUT_MS;
|
|
48336
|
+
await Promise.all(servers.map((server) => this.connectOne(server, timeoutMs, opts.onServer)));
|
|
48337
|
+
}
|
|
48338
|
+
/* BOUNDED, AND SETTLED EXACTLY ONCE.
|
|
48339
|
+
*
|
|
48340
|
+
* Every server connects under one Promise.all, and daemon startup awaits it.
|
|
48341
|
+
* With no bound, one stalled server -- measured: an expired Microsoft 365
|
|
48342
|
+
* token refreshing against a cold service, still waiting at 60 seconds --
|
|
48343
|
+
* left the terminal silent after `mirrors sessions`, indistinguishable from a
|
|
48344
|
+
* crash.
|
|
48345
|
+
*
|
|
48346
|
+
* The deadline covers the whole connect (any credential refresh, initialize,
|
|
48347
|
+
* tools/list), not each request, because several individually bounded steps
|
|
48348
|
+
* in a row can still add up to a hang. The attempt that loses the race keeps
|
|
48349
|
+
* running; its late result is deliberately ignored, so a server reported as
|
|
48350
|
+
* timed out cannot quietly become connected afterwards -- status would then
|
|
48351
|
+
* tell doctor one thing and the registered tools another. */
|
|
48352
|
+
async connectOne(server, timeoutMs, onServer) {
|
|
48353
|
+
let timer;
|
|
48354
|
+
const deadline = new Promise((resolve14) => {
|
|
48355
|
+
timer = setTimeout(() => resolve14("timeout"), timeoutMs);
|
|
48356
|
+
});
|
|
48357
|
+
const attempt = (async () => {
|
|
48358
|
+
const client = this.clientFactory(this.stateDir, server);
|
|
48359
|
+
await client.initialize();
|
|
48360
|
+
const tools = await client.listTools();
|
|
48361
|
+
return { client, tools };
|
|
48362
|
+
})();
|
|
48363
|
+
attempt.catch(() => {
|
|
48364
|
+
});
|
|
48365
|
+
try {
|
|
48366
|
+
const outcome = await Promise.race([attempt, deadline]);
|
|
48367
|
+
if (outcome === "timeout") {
|
|
48368
|
+
this.statuses.set(server.name, { connected: false, toolCount: 0, error: `timed out after ${timeoutMs / 1e3}s` });
|
|
48369
|
+
} else {
|
|
48370
|
+
this.connected.set(server.name, outcome);
|
|
48371
|
+
this.statuses.set(server.name, { connected: true, toolCount: outcome.tools.length });
|
|
48372
|
+
}
|
|
48373
|
+
} catch (err) {
|
|
48374
|
+
this.statuses.set(server.name, {
|
|
48375
|
+
connected: false,
|
|
48376
|
+
toolCount: 0,
|
|
48377
|
+
error: err instanceof Error ? err.message : String(err)
|
|
48378
|
+
});
|
|
48379
|
+
} finally {
|
|
48380
|
+
clearTimeout(timer);
|
|
48381
|
+
}
|
|
48382
|
+
const settled = this.statuses.get(server.name);
|
|
48383
|
+
if (settled) onServer?.({ server: server.name, ...settled });
|
|
48384
|
+
}
|
|
48385
|
+
/** Every tool from every connected OR primed server, paired with the server
|
|
48386
|
+
* it came from, in CONFIGURED order (`serverOrder`) -- not the order
|
|
48387
|
+
* servers happened to finish connecting in, and not insertion order into
|
|
48388
|
+
* `primed` either. Within a server, tools stay in the order that server
|
|
48389
|
+
* returned them. A live connection wins over a primed (cached) entry for
|
|
48390
|
+
* the same server, since it is the fresher truth. A server with neither
|
|
48391
|
+
* either failed to connect or reported zero tools -- `status()` tells the
|
|
48392
|
+
* two apart. */
|
|
48393
|
+
tools() {
|
|
48394
|
+
const out = [];
|
|
48395
|
+
for (const server of this.serverOrder) {
|
|
48396
|
+
const entry = this.connected.get(server);
|
|
48397
|
+
const list = entry ? entry.tools : this.primed.get(server);
|
|
48398
|
+
if (!list) continue;
|
|
48399
|
+
for (const tool of list) out.push({ server, tool });
|
|
48400
|
+
}
|
|
48401
|
+
return out;
|
|
48402
|
+
}
|
|
48403
|
+
/** Seed tool lists from the on-disk cache. NO I/O, no sockets -- this is
|
|
48404
|
+
* what a terminal calls at startup so that `tools()` can answer
|
|
48405
|
+
* immediately, before any server has actually been reached. A primed
|
|
48406
|
+
* server is deliberately NOT `connected`: it can answer what tools exist,
|
|
48407
|
+
* and cannot call one until `ensureConnected()` has run. */
|
|
48408
|
+
primeFromCache(cache) {
|
|
48409
|
+
for (const [name, entry] of Object.entries(cache)) {
|
|
48410
|
+
this.statuses.set(name, { connected: false, toolCount: entry.tools.length });
|
|
48411
|
+
this.primed.set(name, entry.tools);
|
|
48412
|
+
if (!this.serverOrder.includes(name)) this.serverOrder.push(name);
|
|
48413
|
+
}
|
|
48414
|
+
}
|
|
48415
|
+
isConnected(server) {
|
|
48416
|
+
return this.connected.has(server);
|
|
48417
|
+
}
|
|
48418
|
+
connectedServers() {
|
|
48419
|
+
return this.serverOrder.filter((s2) => this.connected.has(s2));
|
|
48420
|
+
}
|
|
48421
|
+
/** Connect ONE server, on demand. Never throws, for the same reason
|
|
48422
|
+
* connectAll never throws: a server that is down must not take the
|
|
48423
|
+
* process with it. Concurrent callers for the same server share one
|
|
48424
|
+
* attempt rather than opening a second socket. */
|
|
48425
|
+
async ensureConnected(server) {
|
|
48426
|
+
if (this.connected.has(server)) return;
|
|
48427
|
+
const inflight = this.connecting.get(server);
|
|
48428
|
+
if (inflight) return inflight;
|
|
48429
|
+
const attempt = (async () => {
|
|
48430
|
+
const servers = await readMcpServers(this.stateDir);
|
|
48431
|
+
const def = servers.find((s2) => s2.name === server);
|
|
48432
|
+
if (!def) return;
|
|
48433
|
+
if (!this.serverOrder.includes(server)) this.serverOrder.push(server);
|
|
48434
|
+
await this.connectOne(def, SERVER_CONNECT_TIMEOUT_MS);
|
|
48435
|
+
})().finally(() => this.connecting.delete(server));
|
|
48436
|
+
this.connecting.set(server, attempt);
|
|
48437
|
+
return attempt;
|
|
48438
|
+
}
|
|
48439
|
+
/**
|
|
48440
|
+
* Call one tool on one server. Fails immediately, naming the server, when
|
|
48441
|
+
* that server never connected -- there is nothing to route the call
|
|
48442
|
+
* through, and pretending otherwise would either stall waiting on a
|
|
48443
|
+
* connection that will never appear or return an empty result the caller
|
|
48444
|
+
* could mistake for a genuine (if empty) tool response.
|
|
48445
|
+
*/
|
|
48446
|
+
async call(server, tool, args) {
|
|
48447
|
+
await this.ensureConnected(server);
|
|
48448
|
+
const entry = this.connected.get(server);
|
|
48449
|
+
if (!entry) {
|
|
48450
|
+
throw new Error(`MCP server "${server}" is not connected -- run: corenel mcp doctor`);
|
|
48451
|
+
}
|
|
48452
|
+
return entry.client.callTool(tool, args);
|
|
48453
|
+
}
|
|
48454
|
+
/** Per-server connection state, for `doctor` (Task 10) to print. A server
|
|
48455
|
+
* with `connected: false` always carries `error` -- there is no code path
|
|
48456
|
+
* that records a failure without a reason, because "no tools" alone tells
|
|
48457
|
+
* the reader nothing about what to fix. */
|
|
48458
|
+
status() {
|
|
48459
|
+
const out = [];
|
|
48460
|
+
for (const server of this.serverOrder) {
|
|
48461
|
+
const s2 = this.statuses.get(server);
|
|
48462
|
+
if (s2) out.push({ server, ...s2 });
|
|
48463
|
+
}
|
|
48464
|
+
return out;
|
|
48465
|
+
}
|
|
48466
|
+
};
|
|
48467
|
+
|
|
48468
|
+
// ../mcp/src/policy.ts
|
|
48469
|
+
function mcpCallAllowed(address, policy) {
|
|
48470
|
+
const allow = policy.tools?.allow ?? [];
|
|
48471
|
+
if (!matchesAnyGlob(address, allow)) {
|
|
48472
|
+
return { ok: false, reason: `${address} is not allowed - add it with: corenel config add mcpAllow ${address}` };
|
|
48473
|
+
}
|
|
48474
|
+
const service = new PermissionService({
|
|
48475
|
+
policy,
|
|
48476
|
+
defaultForReadOnly: "allow",
|
|
48477
|
+
defaultForMutating: "allow"
|
|
48478
|
+
});
|
|
48479
|
+
const level = service.resolveLevel({ name: address, address, mutates: true });
|
|
48480
|
+
if (level !== "allow") {
|
|
48481
|
+
return {
|
|
48482
|
+
ok: false,
|
|
48483
|
+
reason: `${address} resolves to permission "${level}" - MCP tools never prompt; set it to "allow" or remove the rule`
|
|
48484
|
+
};
|
|
48485
|
+
}
|
|
48486
|
+
return { ok: true };
|
|
48487
|
+
}
|
|
48488
|
+
|
|
48489
|
+
// ../mcp/src/toolNames.ts
|
|
48490
|
+
init_protocol();
|
|
48491
|
+
|
|
48492
|
+
// ../mcp/src/asAgentTools.ts
|
|
48493
|
+
function isPlainObject3(v2) {
|
|
48494
|
+
return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
|
|
48495
|
+
}
|
|
48496
|
+
function toParameters(inputSchema) {
|
|
48497
|
+
if (isPlainObject3(inputSchema)) return inputSchema;
|
|
48498
|
+
return { type: "object", properties: {} };
|
|
48499
|
+
}
|
|
48500
|
+
function formatContent(content) {
|
|
48501
|
+
if (typeof content === "string") return content;
|
|
48502
|
+
if (Array.isArray(content)) {
|
|
48503
|
+
const parts = content.map((block) => {
|
|
48504
|
+
if (isPlainObject3(block) && typeof block.text === "string") return block.text;
|
|
48505
|
+
return JSON.stringify(block);
|
|
48506
|
+
});
|
|
48507
|
+
return parts.join("\n");
|
|
48508
|
+
}
|
|
48509
|
+
if (content == null) return "";
|
|
48510
|
+
return JSON.stringify(content);
|
|
48511
|
+
}
|
|
48512
|
+
function asAgentTools(registry2) {
|
|
48513
|
+
const out = [];
|
|
48514
|
+
for (const { server, tool } of registry2.tools()) {
|
|
48515
|
+
if (!canEncodeMcpWireName(server, tool.name)) {
|
|
48516
|
+
console.error(
|
|
48517
|
+
`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`
|
|
48518
|
+
);
|
|
48519
|
+
continue;
|
|
48520
|
+
}
|
|
48521
|
+
out.push(toAgentTool(registry2, server, tool));
|
|
48522
|
+
}
|
|
48523
|
+
return out;
|
|
48524
|
+
}
|
|
48525
|
+
function toAgentTool(registry2, server, tool) {
|
|
48526
|
+
const name = mcpWireName(server, tool.name);
|
|
48527
|
+
const address = mcpToolAddress(server, tool.name);
|
|
48528
|
+
return {
|
|
48529
|
+
name,
|
|
48530
|
+
namespace: "mcp",
|
|
48531
|
+
description: tool.description?.trim() || `MCP tool "${tool.name}" on server "${server}" (${address}).`,
|
|
48532
|
+
parameters: toParameters(tool.inputSchema),
|
|
48533
|
+
mutates: true,
|
|
48534
|
+
async run(args) {
|
|
48535
|
+
const result = await registry2.call(server, tool.name, args);
|
|
48536
|
+
const text = formatContent(result.content);
|
|
48537
|
+
return result.isError ? `Error: ${text}` : text;
|
|
48538
|
+
}
|
|
48539
|
+
};
|
|
48540
|
+
}
|
|
48541
|
+
|
|
48542
|
+
// ../mcp/src/gatedAgentTools.ts
|
|
48543
|
+
function gatedAgentTools(registry2, policy) {
|
|
48544
|
+
const base = asAgentTools(registry2);
|
|
48545
|
+
const byWireName = new Map(base.map((t2) => [t2.name, t2]));
|
|
48546
|
+
const wrapped = [];
|
|
48547
|
+
for (const { server, tool } of registry2.tools()) {
|
|
48548
|
+
if (!canEncodeMcpWireName(server, tool.name)) continue;
|
|
48549
|
+
const name = mcpWireName(server, tool.name);
|
|
48550
|
+
const original = byWireName.get(name);
|
|
48551
|
+
if (!original) continue;
|
|
48552
|
+
const address = mcpToolAddress(server, tool.name);
|
|
48553
|
+
wrapped.push({
|
|
48554
|
+
...original,
|
|
48555
|
+
async run(args, ctx) {
|
|
48556
|
+
const decision = mcpCallAllowed(address, policy);
|
|
48557
|
+
if (!decision.ok) return `Error: ${decision.reason}`;
|
|
48558
|
+
return original.run(args, ctx);
|
|
48559
|
+
}
|
|
48560
|
+
});
|
|
48561
|
+
}
|
|
48562
|
+
return wrapped;
|
|
48563
|
+
}
|
|
48564
|
+
|
|
48565
|
+
// ../mcp/src/oauth/register.ts
|
|
47425
48566
|
async function registerClient(meta, redirectUri, fetchImpl) {
|
|
47426
48567
|
if (!meta.registrationEndpoint) return null;
|
|
47427
48568
|
const res = await fetchImpl(meta.registrationEndpoint, {
|
|
@@ -47451,7 +48592,7 @@ async function registerClient(meta, redirectUri, fetchImpl) {
|
|
|
47451
48592
|
return { clientId, clientSecret };
|
|
47452
48593
|
}
|
|
47453
48594
|
|
|
47454
|
-
// src/
|
|
48595
|
+
// ../mcp/src/oauth/pkce.ts
|
|
47455
48596
|
import { randomBytes as randomBytes5, createHash as createHash3 } from "node:crypto";
|
|
47456
48597
|
function base64url(bytes) {
|
|
47457
48598
|
return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
@@ -47479,7 +48620,7 @@ function authorizationUrl(meta, opts) {
|
|
|
47479
48620
|
return url.toString();
|
|
47480
48621
|
}
|
|
47481
48622
|
|
|
47482
|
-
// src/
|
|
48623
|
+
// ../mcp/src/oauth/loopback.ts
|
|
47483
48624
|
import { createServer } from "node:http";
|
|
47484
48625
|
var PAGE_OK = `<!doctype html><meta charset="utf-8"><title>Signed in</title>
|
|
47485
48626
|
<body style="font:16px system-ui;padding:3rem;text-align:center">
|
|
@@ -47532,6 +48673,17 @@ function awaitRedirect(port, expectedState, opts = {}) {
|
|
|
47532
48673
|
server.once("error", (e2) => {
|
|
47533
48674
|
finish(() => reject(e2 instanceof Error ? e2 : new Error(String(e2))));
|
|
47534
48675
|
});
|
|
48676
|
+
if (opts.signal) {
|
|
48677
|
+
if (opts.signal.aborted) {
|
|
48678
|
+
finish(() => reject(new Error("sign-in cancelled")));
|
|
48679
|
+
} else {
|
|
48680
|
+
opts.signal.addEventListener(
|
|
48681
|
+
"abort",
|
|
48682
|
+
() => finish(() => reject(new Error("sign-in cancelled"))),
|
|
48683
|
+
{ once: true }
|
|
48684
|
+
);
|
|
48685
|
+
}
|
|
48686
|
+
}
|
|
47535
48687
|
v6.on("error", () => {
|
|
47536
48688
|
});
|
|
47537
48689
|
try {
|
|
@@ -47544,6 +48696,15 @@ function awaitRedirect(port, expectedState, opts = {}) {
|
|
|
47544
48696
|
});
|
|
47545
48697
|
});
|
|
47546
48698
|
}
|
|
48699
|
+
function portFree(port) {
|
|
48700
|
+
return new Promise((resolve14) => {
|
|
48701
|
+
const probe = createServer();
|
|
48702
|
+
probe.once("error", () => resolve14(false));
|
|
48703
|
+
probe.listen(port, "127.0.0.1", () => {
|
|
48704
|
+
probe.close(() => resolve14(true));
|
|
48705
|
+
});
|
|
48706
|
+
});
|
|
48707
|
+
}
|
|
47547
48708
|
function getFreePort() {
|
|
47548
48709
|
return new Promise((resolve14, reject) => {
|
|
47549
48710
|
const probe = createServer();
|
|
@@ -47555,93 +48716,16 @@ function getFreePort() {
|
|
|
47555
48716
|
});
|
|
47556
48717
|
}
|
|
47557
48718
|
|
|
47558
|
-
// src/
|
|
47559
|
-
function parseTokenResponse(body2) {
|
|
47560
|
-
if (typeof body2 !== "object" || body2 === null || Array.isArray(body2)) {
|
|
47561
|
-
throw new Error("token endpoint returned a malformed response");
|
|
47562
|
-
}
|
|
47563
|
-
const r2 = body2;
|
|
47564
|
-
if (typeof r2.access_token !== "string" || r2.access_token.length === 0) {
|
|
47565
|
-
throw new Error("token endpoint response did not include an access_token");
|
|
47566
|
-
}
|
|
47567
|
-
return {
|
|
47568
|
-
access_token: r2.access_token,
|
|
47569
|
-
...typeof r2.refresh_token === "string" ? { refresh_token: r2.refresh_token } : {},
|
|
47570
|
-
...typeof r2.expires_in === "number" ? { expires_in: r2.expires_in } : {},
|
|
47571
|
-
...typeof r2.scope === "string" ? { scope: r2.scope } : {}
|
|
47572
|
-
};
|
|
47573
|
-
}
|
|
47574
|
-
async function postToken(endpoint, body2, fetchImpl) {
|
|
47575
|
-
const res = await fetchImpl(endpoint, {
|
|
47576
|
-
method: "POST",
|
|
47577
|
-
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
47578
|
-
body: body2.toString()
|
|
47579
|
-
});
|
|
47580
|
-
if (!res.ok) {
|
|
47581
|
-
throw new Error(`token request failed at ${endpoint} (HTTP ${res.status})`);
|
|
47582
|
-
}
|
|
47583
|
-
return parseTokenResponse(await res.json());
|
|
47584
|
-
}
|
|
47585
|
-
async function exchangeCode(meta, opts, fetchImpl = fetch, now2 = Date.now) {
|
|
47586
|
-
const body2 = new URLSearchParams({
|
|
47587
|
-
grant_type: "authorization_code",
|
|
47588
|
-
client_id: opts.clientId,
|
|
47589
|
-
code: opts.code,
|
|
47590
|
-
code_verifier: opts.verifier,
|
|
47591
|
-
redirect_uri: opts.redirectUri,
|
|
47592
|
-
resource: opts.resource
|
|
47593
|
-
});
|
|
47594
|
-
if (opts.clientSecret) body2.set("client_secret", opts.clientSecret);
|
|
47595
|
-
const tokens = await postToken(meta.tokenEndpoint, body2, fetchImpl);
|
|
47596
|
-
return {
|
|
47597
|
-
kind: "oauth",
|
|
47598
|
-
accessToken: tokens.access_token,
|
|
47599
|
-
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {},
|
|
47600
|
-
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
47601
|
-
clientId: opts.clientId,
|
|
47602
|
-
...opts.clientSecret ? { clientSecret: opts.clientSecret } : {},
|
|
47603
|
-
authServer: opts.authServer,
|
|
47604
|
-
...tokens.scope ? { scope: tokens.scope } : {}
|
|
47605
|
-
};
|
|
47606
|
-
}
|
|
47607
|
-
async function refresh(meta, cred, fetchImpl = fetch, now2 = Date.now) {
|
|
47608
|
-
if (!cred.refreshToken) {
|
|
47609
|
-
throw new Error("credential has no refresh token to refresh with -- sign in again");
|
|
47610
|
-
}
|
|
47611
|
-
const body2 = new URLSearchParams({
|
|
47612
|
-
grant_type: "refresh_token",
|
|
47613
|
-
refresh_token: cred.refreshToken,
|
|
47614
|
-
client_id: cred.clientId
|
|
47615
|
-
});
|
|
47616
|
-
if (cred.clientSecret) body2.set("client_secret", cred.clientSecret);
|
|
47617
|
-
const tokens = await postToken(meta.tokenEndpoint, body2, fetchImpl);
|
|
47618
|
-
return {
|
|
47619
|
-
kind: "oauth",
|
|
47620
|
-
accessToken: tokens.access_token,
|
|
47621
|
-
// Keep the old refresh token when the server did not send a new one.
|
|
47622
|
-
refreshToken: tokens.refresh_token ?? cred.refreshToken,
|
|
47623
|
-
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
47624
|
-
clientId: cred.clientId,
|
|
47625
|
-
...cred.clientSecret ? { clientSecret: cred.clientSecret } : {},
|
|
47626
|
-
authServer: cred.authServer,
|
|
47627
|
-
scope: tokens.scope ?? cred.scope
|
|
47628
|
-
};
|
|
47629
|
-
}
|
|
47630
|
-
var EXPIRY_SKEW_MS = 6e4;
|
|
47631
|
-
function isExpired(cred, nowMs) {
|
|
47632
|
-
if (cred.expiresAt === void 0) return false;
|
|
47633
|
-
return cred.expiresAt - nowMs <= EXPIRY_SKEW_MS;
|
|
47634
|
-
}
|
|
47635
|
-
|
|
47636
|
-
// src/mcp/oauth/login.ts
|
|
48719
|
+
// ../mcp/src/oauth/login.ts
|
|
47637
48720
|
var defaultOpenBrowser = async (url) => {
|
|
47638
|
-
const { openBrowser: openBrowser3 } = await Promise.resolve().then(() => (
|
|
48721
|
+
const { openBrowser: openBrowser3 } = await Promise.resolve().then(() => (init_tools_node(), tools_node_exports));
|
|
47639
48722
|
await openBrowser3(url);
|
|
47640
48723
|
};
|
|
47641
48724
|
async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
47642
48725
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
47643
48726
|
const openBrowserFn = deps.openBrowser ?? defaultOpenBrowser;
|
|
47644
48727
|
const now2 = deps.now ?? Date.now;
|
|
48728
|
+
const redirectAbort = new AbortController();
|
|
47645
48729
|
const signInUrl = server.authUrl ?? server.url;
|
|
47646
48730
|
try {
|
|
47647
48731
|
const discovered = await discoverAuthServer(
|
|
@@ -47662,6 +48746,13 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
|
47662
48746
|
const meta = discovered.scopes ? { ...asMeta, scopesSupported: discovered.scopes } : asMeta;
|
|
47663
48747
|
const cached = await readMcpClient(stateDir, server.name);
|
|
47664
48748
|
const port = cached?.redirectPort ?? await getFreePort();
|
|
48749
|
+
if (cached?.redirectPort !== void 0 && !await portFree(cached.redirectPort)) {
|
|
48750
|
+
return {
|
|
48751
|
+
ok: false,
|
|
48752
|
+
reason: "failed",
|
|
48753
|
+
message: `the redirect port ${cached.redirectPort} is in use, and "${server.name}" is registered against exactly that port -- free it and sign in again`
|
|
48754
|
+
};
|
|
48755
|
+
}
|
|
47665
48756
|
const redirectUri = `http://localhost:${port}/callback`;
|
|
47666
48757
|
const registered2 = cached && cached.authServer === discovered.authServer ? { clientId: cached.clientId, ...cached.clientSecret ? { clientSecret: cached.clientSecret } : {} } : await registerClient(
|
|
47667
48758
|
meta,
|
|
@@ -47672,7 +48763,25 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
|
47672
48763
|
await writeMcpClient(stateDir, server.name, {
|
|
47673
48764
|
clientId: registered2.clientId,
|
|
47674
48765
|
...registered2.clientSecret ? { clientSecret: registered2.clientSecret } : {},
|
|
47675
|
-
authServer: discovered.authServer
|
|
48766
|
+
authServer: discovered.authServer,
|
|
48767
|
+
/* PIN THE PORT WE JUST REGISTERED WITH.
|
|
48768
|
+
*
|
|
48769
|
+
* RFC 8252 s7.3 obliges an authorization server to accept any port on a
|
|
48770
|
+
* loopback redirect, and most do -- but not all, and the ones that do
|
|
48771
|
+
* not fail in the worst possible way. Atlassian's `auth.atlassian.com`
|
|
48772
|
+
* (Trello's authorization server, distinct from `mcp.atlassian.com`)
|
|
48773
|
+
* matches the redirect URI exactly. Registering with one ephemeral port
|
|
48774
|
+
* and then authorizing with a fresh one on the next sign-in produced
|
|
48775
|
+
* "The app's callback URL is invalid", from the vendor's own page,
|
|
48776
|
+
* BEFORE any token exchange -- so nothing came back for the
|
|
48777
|
+
* invalid_client recovery below to catch, and the flow simply timed out
|
|
48778
|
+
* waiting for a redirect that was never going to arrive.
|
|
48779
|
+
*
|
|
48780
|
+
* Persisting it costs nothing where the port is ignored, and is the
|
|
48781
|
+
* whole difference where it is not. The field already existed for
|
|
48782
|
+
* operator-set ports (`corenel mcp set-client --redirect-port`); it was
|
|
48783
|
+
* read here and never written by this path. */
|
|
48784
|
+
redirectPort: port
|
|
47676
48785
|
});
|
|
47677
48786
|
}
|
|
47678
48787
|
if (!registered2) {
|
|
@@ -47693,7 +48802,7 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
|
47693
48802
|
resource: discovered.resource
|
|
47694
48803
|
});
|
|
47695
48804
|
deps.onAuthUrl?.(authUrl);
|
|
47696
|
-
const redirectP = awaitRedirect(port, state);
|
|
48805
|
+
const redirectP = awaitRedirect(port, state, { signal: redirectAbort.signal });
|
|
47697
48806
|
redirectP.catch(() => {
|
|
47698
48807
|
});
|
|
47699
48808
|
await openBrowserFn(authUrl);
|
|
@@ -47715,6 +48824,7 @@ async function loginToMcpServer(stateDir, server, deps = {}) {
|
|
|
47715
48824
|
await writeMcpCredential(stateDir, server.name, cred);
|
|
47716
48825
|
return { ok: true };
|
|
47717
48826
|
} catch (e2) {
|
|
48827
|
+
redirectAbort.abort();
|
|
47718
48828
|
const message = e2 instanceof Error ? e2.message : String(e2);
|
|
47719
48829
|
if (/invalid_client|unauthorized_client|client.{0,20}not.{0,10}found/i.test(message)) {
|
|
47720
48830
|
await clearMcpClient(stateDir, server.name).catch(() => {
|
|
@@ -48211,7 +49321,7 @@ function persistAddedRoots(controller, stateDir, log) {
|
|
|
48211
49321
|
import { spawn as spawn7 } from "node:child_process";
|
|
48212
49322
|
import { promises as fs16 } from "node:fs";
|
|
48213
49323
|
import { join as join20, resolve as resolve13 } from "node:path";
|
|
48214
|
-
var
|
|
49324
|
+
var DEFAULT_TIMEOUT_MS4 = 10 * 6e4;
|
|
48215
49325
|
var ALLOWED_SCHEMES = ["https://", "ssh://", "git+ssh://"];
|
|
48216
49326
|
var SCP_LIKE = /^[A-Za-z0-9_.-]+@[A-Za-z0-9_.-]+:[A-Za-z0-9_./~-]+$/;
|
|
48217
49327
|
function assertCloneUrl(url) {
|
|
@@ -48275,8 +49385,8 @@ function makeGitRunner(bin = "git") {
|
|
|
48275
49385
|
const timer = setTimeout(() => {
|
|
48276
49386
|
child.kill("SIGKILL");
|
|
48277
49387
|
settle({ code: -1, stdout, stderr: `${stderr}
|
|
48278
|
-
git timed out after ${opts?.timeoutMs ??
|
|
48279
|
-
}, opts?.timeoutMs ??
|
|
49388
|
+
git timed out after ${opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS4}ms` });
|
|
49389
|
+
}, opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS4);
|
|
48280
49390
|
timer.unref?.();
|
|
48281
49391
|
child.on("error", (e2) => {
|
|
48282
49392
|
clearTimeout(timer);
|
|
@@ -48385,7 +49495,7 @@ var RpcError = class extends Error {
|
|
|
48385
49495
|
if (shape.data !== void 0) this.data = shape.data;
|
|
48386
49496
|
}
|
|
48387
49497
|
};
|
|
48388
|
-
function
|
|
49498
|
+
function isRecord3(v2) {
|
|
48389
49499
|
return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
|
|
48390
49500
|
}
|
|
48391
49501
|
var JsonRpcPeer = class {
|
|
@@ -48423,7 +49533,7 @@ var JsonRpcPeer = class {
|
|
|
48423
49533
|
this.log(`acp: dropped unparseable frame (${line.length} bytes)`);
|
|
48424
49534
|
return;
|
|
48425
49535
|
}
|
|
48426
|
-
if (!
|
|
49536
|
+
if (!isRecord3(msg)) {
|
|
48427
49537
|
this.log("acp: dropped non-object frame");
|
|
48428
49538
|
return;
|
|
48429
49539
|
}
|
|
@@ -48445,7 +49555,7 @@ var JsonRpcPeer = class {
|
|
|
48445
49555
|
return;
|
|
48446
49556
|
}
|
|
48447
49557
|
this.pending.delete(id);
|
|
48448
|
-
if ("error" in msg &&
|
|
49558
|
+
if ("error" in msg && isRecord3(msg.error)) {
|
|
48449
49559
|
const e2 = msg.error;
|
|
48450
49560
|
p2.reject(new RpcError({
|
|
48451
49561
|
code: typeof e2.code === "number" ? e2.code : RPC_INTERNAL_ERROR,
|
|
@@ -49337,7 +50447,6 @@ function textOf(prompt) {
|
|
|
49337
50447
|
}
|
|
49338
50448
|
|
|
49339
50449
|
// src/acp/agentCli.ts
|
|
49340
|
-
init_store();
|
|
49341
50450
|
function elog(line) {
|
|
49342
50451
|
process.stderr.write(`${line}
|
|
49343
50452
|
`);
|
|
@@ -49385,434 +50494,239 @@ async function runAcpAgent(argv) {
|
|
|
49385
50494
|
});
|
|
49386
50495
|
}
|
|
49387
50496
|
|
|
49388
|
-
// src/
|
|
49389
|
-
|
|
49390
|
-
init_store();
|
|
49391
|
-
init_fileService();
|
|
49392
|
-
import { relative as relative7, resolve as resolvePath } from "node:path";
|
|
50497
|
+
// src/auth/login.ts
|
|
50498
|
+
import { createServer as createServer2 } from "node:http";
|
|
49393
50499
|
|
|
49394
|
-
// src/
|
|
49395
|
-
|
|
49396
|
-
|
|
49397
|
-
|
|
49398
|
-
var McpHttpClient = class {
|
|
49399
|
-
constructor(server, auth, fetchImpl = fetch) {
|
|
49400
|
-
this.server = server;
|
|
49401
|
-
this.auth = auth;
|
|
49402
|
-
this.fetchImpl = fetchImpl;
|
|
49403
|
-
}
|
|
49404
|
-
nextRequestId = 1;
|
|
49405
|
-
sessionId;
|
|
49406
|
-
async initialize() {
|
|
49407
|
-
await this.send("initialize", {
|
|
49408
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
49409
|
-
capabilities: {},
|
|
49410
|
-
clientInfo: CLIENT_INFO
|
|
49411
|
-
});
|
|
49412
|
-
}
|
|
49413
|
-
async listTools() {
|
|
49414
|
-
const result = await this.send("tools/list", {});
|
|
49415
|
-
const tools = isRecord3(result) ? result.tools : void 0;
|
|
49416
|
-
if (!Array.isArray(tools)) return [];
|
|
49417
|
-
return tools.filter(isMcpTool);
|
|
49418
|
-
}
|
|
49419
|
-
async callTool(name, args) {
|
|
49420
|
-
const result = await this.send("tools/call", { name, arguments: args });
|
|
49421
|
-
const record = isRecord3(result) ? result : {};
|
|
49422
|
-
return { content: record.content, isError: record.isError === true };
|
|
49423
|
-
}
|
|
49424
|
-
/** Send one JSON-RPC request, retrying exactly once on a 401 that the
|
|
49425
|
-
* AuthProvider says it recovered from. `isRetry` prevents a second retry
|
|
49426
|
-
* loop -- a credential that fails twice is refused, not flaky. */
|
|
49427
|
-
async send(method, params, isRetry = false) {
|
|
49428
|
-
const headers = await this.buildHeaders();
|
|
49429
|
-
const id = this.nextRequestId++;
|
|
49430
|
-
const res = await this.request(headers, id, method, params);
|
|
49431
|
-
if (res.status === 401) {
|
|
49432
|
-
if (!isRetry && await this.auth.onUnauthorized()) {
|
|
49433
|
-
return this.send(method, params, true);
|
|
49434
|
-
}
|
|
49435
|
-
throw new Error(
|
|
49436
|
-
`MCP server "${this.server.name}" refused the credential (401) -- run: corenel mcp login ${this.server.name}`
|
|
49437
|
-
);
|
|
49438
|
-
}
|
|
49439
|
-
const sessionId = res.headers.get(SESSION_HEADER);
|
|
49440
|
-
if (sessionId) this.sessionId = sessionId;
|
|
49441
|
-
if (!res.ok) {
|
|
49442
|
-
throw new Error(`MCP server "${this.server.name}" returned HTTP ${res.status}`);
|
|
49443
|
-
}
|
|
49444
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
49445
|
-
let body2;
|
|
49446
|
-
if (contentType.includes("text/event-stream")) {
|
|
49447
|
-
body2 = await this.readSseResponse(res, id);
|
|
49448
|
-
} else if (contentType.includes("application/json")) {
|
|
49449
|
-
body2 = await res.json();
|
|
49450
|
-
} else {
|
|
49451
|
-
throw new Error(
|
|
49452
|
-
`MCP server "${this.server.name}" answered with an unexpected content-type: "${contentType || "(none)"}"`
|
|
49453
|
-
);
|
|
49454
|
-
}
|
|
49455
|
-
if (body2.error) {
|
|
49456
|
-
throw new Error(`MCP server "${this.server.name}" returned error ${body2.error.code}: ${body2.error.message}`);
|
|
49457
|
-
}
|
|
49458
|
-
return body2.result;
|
|
49459
|
-
}
|
|
49460
|
-
/** Read an SSE body until the JSON-RPC response for `id` arrives.
|
|
49461
|
-
*
|
|
49462
|
-
* Events are separated by a blank line; an event's `data:` lines are joined
|
|
49463
|
-
* with newlines, per the SSE spec, and parsed as one JSON-RPC message.
|
|
49464
|
-
* Notifications (no `id`) and responses to other ids are skipped. The reader
|
|
49465
|
-
* is cancelled once the answer is in, so a server that keeps the stream open
|
|
49466
|
-
* after replying cannot hold this call open with it. */
|
|
49467
|
-
async readSseResponse(res, id) {
|
|
49468
|
-
if (!res.body) {
|
|
49469
|
-
throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
|
|
49470
|
-
}
|
|
49471
|
-
const reader = res.body.getReader();
|
|
49472
|
-
const decoder = new TextDecoder();
|
|
49473
|
-
let buffer = "";
|
|
49474
|
-
try {
|
|
49475
|
-
for (; ; ) {
|
|
49476
|
-
const { done, value } = await reader.read();
|
|
49477
|
-
if (value) buffer += decoder.decode(value, { stream: true });
|
|
49478
|
-
buffer = buffer.replace(/\r\n/g, "\n");
|
|
49479
|
-
let boundary = buffer.indexOf("\n\n");
|
|
49480
|
-
while (boundary !== -1) {
|
|
49481
|
-
const event = buffer.slice(0, boundary);
|
|
49482
|
-
buffer = buffer.slice(boundary + 2);
|
|
49483
|
-
const match = this.matchSseEvent(event, id);
|
|
49484
|
-
if (match) return match;
|
|
49485
|
-
boundary = buffer.indexOf("\n\n");
|
|
49486
|
-
}
|
|
49487
|
-
if (done) {
|
|
49488
|
-
const tail3 = buffer.trim() ? this.matchSseEvent(buffer, id) : null;
|
|
49489
|
-
if (tail3) return tail3;
|
|
49490
|
-
throw new Error(`MCP server "${this.server.name}" closed the stream without a response`);
|
|
49491
|
-
}
|
|
49492
|
-
}
|
|
49493
|
-
} finally {
|
|
49494
|
-
reader.cancel().catch(() => {
|
|
49495
|
-
});
|
|
49496
|
-
}
|
|
49497
|
-
}
|
|
49498
|
-
/** The JSON-RPC response in one SSE event, if it answers `id`; else null. */
|
|
49499
|
-
matchSseEvent(event, id) {
|
|
49500
|
-
const data = event.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).replace(/^ /, "")).join("\n");
|
|
49501
|
-
if (!data) return null;
|
|
49502
|
-
let parsed;
|
|
49503
|
-
try {
|
|
49504
|
-
parsed = JSON.parse(data);
|
|
49505
|
-
} catch {
|
|
49506
|
-
return null;
|
|
49507
|
-
}
|
|
49508
|
-
if (!isRecord3(parsed) || parsed.id !== id) return null;
|
|
49509
|
-
if (!("result" in parsed) && !("error" in parsed)) return null;
|
|
49510
|
-
return parsed;
|
|
49511
|
-
}
|
|
49512
|
-
/** The bare `fetch()` call, wrapped -- every OTHER throw in `send` already
|
|
49513
|
-
* names the server; this is the one call whose own failures (DNS miss,
|
|
49514
|
-
* connection refused, TLS failure) never reach `send`'s response-handling
|
|
49515
|
-
* at all. Left unguarded, node's bare `fetch failed` -- naming neither the
|
|
49516
|
-
* URL nor the actual cause -- propagates straight out of `send`, into
|
|
49517
|
-
* `registry.ts`'s `status().error`, and out of `doctor`, the one command
|
|
49518
|
-
* whose entire job is diagnosis. `err.cause` carries the real reason
|
|
49519
|
-
* (ECONNREFUSED, ENOTFOUND, a TLS alert); undici puts it there rather than
|
|
49520
|
-
* in `err.message`. */
|
|
49521
|
-
async request(headers, id, method, params) {
|
|
49522
|
-
try {
|
|
49523
|
-
return await this.fetchImpl(this.server.url, {
|
|
49524
|
-
method: "POST",
|
|
49525
|
-
headers,
|
|
49526
|
-
body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
|
|
49527
|
-
});
|
|
49528
|
-
} catch (err) {
|
|
49529
|
-
const cause = err instanceof Error && err.cause instanceof Error ? err.cause.message : err instanceof Error ? err.message : String(err);
|
|
49530
|
-
throw new Error(`MCP server "${this.server.name}" (${this.server.url}) is unreachable: ${cause}`);
|
|
49531
|
-
}
|
|
49532
|
-
}
|
|
49533
|
-
async buildHeaders() {
|
|
49534
|
-
const headers = new Headers();
|
|
49535
|
-
headers.set("Content-Type", "application/json");
|
|
49536
|
-
headers.set("Accept", "application/json, text/event-stream");
|
|
49537
|
-
for (const h2 of await this.auth.headers()) headers.set(h2.name, h2.value);
|
|
49538
|
-
if (this.sessionId) headers.set(SESSION_HEADER, this.sessionId);
|
|
49539
|
-
return headers;
|
|
49540
|
-
}
|
|
49541
|
-
};
|
|
49542
|
-
function isRecord3(value) {
|
|
49543
|
-
return typeof value === "object" && value !== null;
|
|
50500
|
+
// src/auth/pkce.ts
|
|
50501
|
+
import { createHash as createHash4, randomBytes as randomBytes6 } from "node:crypto";
|
|
50502
|
+
function base64url2(buf) {
|
|
50503
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
49544
50504
|
}
|
|
49545
|
-
function
|
|
49546
|
-
return
|
|
50505
|
+
function createVerifier(bytes = 32) {
|
|
50506
|
+
return base64url2(randomBytes6(bytes));
|
|
50507
|
+
}
|
|
50508
|
+
function challengeFor(verifier) {
|
|
50509
|
+
return base64url2(createHash4("sha256").update(verifier).digest());
|
|
50510
|
+
}
|
|
50511
|
+
function createState() {
|
|
50512
|
+
return base64url2(randomBytes6(16));
|
|
50513
|
+
}
|
|
50514
|
+
function createPkce() {
|
|
50515
|
+
const verifier = createVerifier();
|
|
50516
|
+
return { verifier, challenge: challengeFor(verifier), method: "S256", state: createState() };
|
|
50517
|
+
}
|
|
50518
|
+
function authorizeUrl(p2) {
|
|
50519
|
+
const u2 = new URL("/oauth/authorize", p2.issuer);
|
|
50520
|
+
u2.searchParams.set("response_type", "code");
|
|
50521
|
+
u2.searchParams.set("client_id", p2.clientId);
|
|
50522
|
+
u2.searchParams.set("redirect_uri", p2.redirectUri);
|
|
50523
|
+
u2.searchParams.set("code_challenge", p2.pkce.challenge);
|
|
50524
|
+
u2.searchParams.set("code_challenge_method", p2.pkce.method);
|
|
50525
|
+
u2.searchParams.set("state", p2.pkce.state);
|
|
50526
|
+
u2.searchParams.set("scope", p2.scope ?? "profile email");
|
|
50527
|
+
return u2.toString();
|
|
50528
|
+
}
|
|
50529
|
+
function stateMatches(expected, got) {
|
|
50530
|
+
if (!got || got.length !== expected.length) return false;
|
|
50531
|
+
let diff = 0;
|
|
50532
|
+
for (let i2 = 0; i2 < expected.length; i2++) diff |= expected.charCodeAt(i2) ^ got.charCodeAt(i2);
|
|
50533
|
+
return diff === 0;
|
|
49547
50534
|
}
|
|
49548
50535
|
|
|
49549
|
-
// src/
|
|
49550
|
-
|
|
49551
|
-
|
|
49552
|
-
|
|
49553
|
-
|
|
50536
|
+
// src/auth/login.ts
|
|
50537
|
+
var DEFAULT_TIMEOUT_MS5 = 5 * 60 * 1e3;
|
|
50538
|
+
var PAGE_OK2 = `<!doctype html><meta charset="utf-8"><title>Signed in</title>
|
|
50539
|
+
<body style="font:16px system-ui;padding:3rem;text-align:center">
|
|
50540
|
+
<h1>Signed in to Corenel</h1><p>You can close this tab and return to your terminal.</p></body>`;
|
|
50541
|
+
var PAGE_FAIL2 = `<!doctype html><meta charset="utf-8"><title>Sign-in failed</title>
|
|
50542
|
+
<body style="font:16px system-ui;padding:3rem;text-align:center">
|
|
50543
|
+
<h1>Sign-in failed</h1><p>Return to your terminal for details.</p></body>`;
|
|
50544
|
+
function awaitCallback(pkce, port, timeoutMs) {
|
|
50545
|
+
let resolveUri;
|
|
50546
|
+
let resolveResult;
|
|
50547
|
+
let rejectResult;
|
|
50548
|
+
const redirectUri = new Promise((res) => {
|
|
50549
|
+
resolveUri = res;
|
|
50550
|
+
});
|
|
50551
|
+
const result = new Promise((res, rej) => {
|
|
50552
|
+
resolveResult = res;
|
|
50553
|
+
rejectResult = rej;
|
|
50554
|
+
});
|
|
50555
|
+
const server = createServer2((req, res) => {
|
|
50556
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
50557
|
+
if (url.pathname !== "/callback") {
|
|
50558
|
+
res.writeHead(404).end();
|
|
50559
|
+
return;
|
|
49554
50560
|
}
|
|
49555
|
-
const
|
|
49556
|
-
const
|
|
49557
|
-
|
|
49558
|
-
|
|
49559
|
-
|
|
49560
|
-
|
|
49561
|
-
|
|
49562
|
-
const cred = await readMcpCredential(stateDir, server.name);
|
|
49563
|
-
if (!cred && !server.hosted) {
|
|
49564
|
-
throw new Error(`No credential stored for MCP server "${server.name}" -- run: corenel mcp login ${server.name} (or set-header, for a static token).`);
|
|
49565
|
-
}
|
|
49566
|
-
const mcpHeaders = !cred ? [] : cred.kind === "headers" ? cred.headers : [{ name: "Authorization", value: `Bearer ${(isExpired(cred, Date.now()) ? await doRefresh(cred) : cred).accessToken}` }];
|
|
49567
|
-
if (!server.hosted) return mcpHeaders;
|
|
49568
|
-
const moved = mcpHeaders.map((h2) => h2.name.toLowerCase() === "authorization" ? { name: "X-MCP-Authorization", value: h2.value } : h2);
|
|
49569
|
-
const account = await accountToken();
|
|
49570
|
-
return account ? [...moved, { name: "Authorization", value: `Bearer ${account}` }] : moved;
|
|
49571
|
-
},
|
|
49572
|
-
async onUnauthorized() {
|
|
49573
|
-
const cred = await readMcpCredential(stateDir, server.name);
|
|
49574
|
-
if (!cred || cred.kind !== "oauth") return false;
|
|
49575
|
-
try {
|
|
49576
|
-
await doRefresh(cred);
|
|
49577
|
-
return true;
|
|
49578
|
-
} catch {
|
|
49579
|
-
return false;
|
|
49580
|
-
}
|
|
50561
|
+
const err = url.searchParams.get("error");
|
|
50562
|
+
const code2 = url.searchParams.get("code");
|
|
50563
|
+
const state = url.searchParams.get("state");
|
|
50564
|
+
if (!stateMatches(pkce.state, state)) {
|
|
50565
|
+
res.writeHead(400, { "content-type": "text/html" }).end(PAGE_FAIL2);
|
|
50566
|
+
rejectResult(new Error("state mismatch \u2014 the callback did not match this sign-in request"));
|
|
50567
|
+
return;
|
|
49581
50568
|
}
|
|
49582
|
-
|
|
49583
|
-
}
|
|
49584
|
-
|
|
49585
|
-
|
|
49586
|
-
init_store();
|
|
49587
|
-
|
|
49588
|
-
// src/mcp/timedFetch.ts
|
|
49589
|
-
var AUTH_REQUEST_TIMEOUT_MS = 15e3;
|
|
49590
|
-
function timedFetch(ms, inner = fetch) {
|
|
49591
|
-
return async (input, init2 = {}) => {
|
|
49592
|
-
const deadline = AbortSignal.timeout(ms);
|
|
49593
|
-
const signal = init2.signal ? AbortSignal.any([init2.signal, deadline]) : deadline;
|
|
49594
|
-
try {
|
|
49595
|
-
return await inner(input, { ...init2, signal });
|
|
49596
|
-
} catch (err) {
|
|
49597
|
-
if (deadline.aborted && !init2.signal?.aborted) {
|
|
49598
|
-
throw new Error(`no answer within ${ms / 1e3}s`, { cause: err });
|
|
49599
|
-
}
|
|
49600
|
-
throw err;
|
|
50569
|
+
if (err) {
|
|
50570
|
+
res.writeHead(400, { "content-type": "text/html" }).end(PAGE_FAIL2);
|
|
50571
|
+
rejectResult(new Error(`authorization failed: ${err}`));
|
|
50572
|
+
return;
|
|
49601
50573
|
}
|
|
50574
|
+
if (!code2) {
|
|
50575
|
+
res.writeHead(400, { "content-type": "text/html" }).end(PAGE_FAIL2);
|
|
50576
|
+
rejectResult(new Error("callback carried no authorization code"));
|
|
50577
|
+
return;
|
|
50578
|
+
}
|
|
50579
|
+
res.writeHead(200, { "content-type": "text/html" }).end(PAGE_OK2);
|
|
50580
|
+
resolveResult({ code: code2 });
|
|
50581
|
+
});
|
|
50582
|
+
const timer = setTimeout(() => {
|
|
50583
|
+
rejectResult(new Error("timed out waiting for the browser callback"));
|
|
50584
|
+
}, timeoutMs);
|
|
50585
|
+
timer.unref?.();
|
|
50586
|
+
const close2 = () => {
|
|
50587
|
+
clearTimeout(timer);
|
|
50588
|
+
server.close();
|
|
49602
50589
|
};
|
|
50590
|
+
void result.finally(close2).catch(() => {
|
|
50591
|
+
});
|
|
50592
|
+
server.listen(port, "127.0.0.1", () => {
|
|
50593
|
+
const addr = server.address();
|
|
50594
|
+
const bound = typeof addr === "object" && addr ? addr.port : port;
|
|
50595
|
+
resolveUri(`http://127.0.0.1:${bound}/callback`);
|
|
50596
|
+
});
|
|
50597
|
+
return { redirectUri, result, close: close2 };
|
|
49603
50598
|
}
|
|
49604
|
-
|
|
49605
|
-
|
|
49606
|
-
|
|
49607
|
-
|
|
49608
|
-
|
|
49609
|
-
|
|
49610
|
-
|
|
49611
|
-
|
|
49612
|
-
}
|
|
49613
|
-
}
|
|
49614
|
-
function defaultClientFactory(stateDir, server) {
|
|
49615
|
-
return new McpHttpClient(
|
|
49616
|
-
server,
|
|
49617
|
-
authProviderFor(stateDir, server, timedFetch(AUTH_REQUEST_TIMEOUT_MS), () => accountTokenFor(stateDir))
|
|
50599
|
+
async function login(deps) {
|
|
50600
|
+
const log = deps.log ?? (() => void 0);
|
|
50601
|
+
const now2 = deps.now ?? Date.now;
|
|
50602
|
+
const pkce = createPkce();
|
|
50603
|
+
const { redirectUri: uriP, result, close: close2 } = awaitCallback(
|
|
50604
|
+
pkce,
|
|
50605
|
+
deps.config.port ?? 0,
|
|
50606
|
+
deps.timeoutMs ?? DEFAULT_TIMEOUT_MS5
|
|
49618
50607
|
);
|
|
49619
|
-
|
|
49620
|
-
|
|
49621
|
-
|
|
49622
|
-
|
|
49623
|
-
|
|
49624
|
-
|
|
49625
|
-
|
|
49626
|
-
|
|
49627
|
-
statuses = /* @__PURE__ */ new Map();
|
|
49628
|
-
// The order the operator wrote servers in daemon.json, captured once per
|
|
49629
|
-
// connectAll() -- `tools()` walks THIS, never `connected`'s Map iteration
|
|
49630
|
-
// order. `connectOne` runs concurrently for every server (Promise.all), so
|
|
49631
|
-
// Map insertion order is race order: whichever server's initialize+list
|
|
49632
|
-
// happened to resolve first. That is fine for `connected` (a lookup table,
|
|
49633
|
-
// order never read) but `tools()` is a list a model/doctor/policy consumes
|
|
49634
|
-
// in order, and race order means a different tool ordering on every daemon
|
|
49635
|
-
// boot -- exactly the kind of bug that surfaces far from here and is never
|
|
49636
|
-
// traced back. Configured order is also information the operator supplied
|
|
49637
|
-
// deliberately; sorting alphabetically would discard it.
|
|
49638
|
-
serverOrder = [];
|
|
49639
|
-
/**
|
|
49640
|
-
* Connect to every configured server and cache its tools. Never throws --
|
|
49641
|
-
* a server that fails to initialize, or fails listing tools, is recorded
|
|
49642
|
-
* in `status()` with zero tools and the daemon keeps starting.
|
|
49643
|
-
*/
|
|
49644
|
-
async connectAll(opts = {}) {
|
|
49645
|
-
this.connected.clear();
|
|
49646
|
-
this.statuses.clear();
|
|
49647
|
-
const servers = await readMcpServers(this.stateDir);
|
|
49648
|
-
this.serverOrder = servers.map((s2) => s2.name);
|
|
49649
|
-
const timeoutMs = opts.timeoutMs ?? SERVER_CONNECT_TIMEOUT_MS;
|
|
49650
|
-
await Promise.all(servers.map((server) => this.connectOne(server, timeoutMs, opts.onServer)));
|
|
49651
|
-
}
|
|
49652
|
-
/* BOUNDED, AND SETTLED EXACTLY ONCE.
|
|
49653
|
-
*
|
|
49654
|
-
* Every server connects under one Promise.all, and daemon startup awaits it.
|
|
49655
|
-
* With no bound, one stalled server -- measured: an expired Microsoft 365
|
|
49656
|
-
* token refreshing against a cold service, still waiting at 60 seconds --
|
|
49657
|
-
* left the terminal silent after `mirrors sessions`, indistinguishable from a
|
|
49658
|
-
* crash.
|
|
49659
|
-
*
|
|
49660
|
-
* The deadline covers the whole connect (any credential refresh, initialize,
|
|
49661
|
-
* tools/list), not each request, because several individually bounded steps
|
|
49662
|
-
* in a row can still add up to a hang. The attempt that loses the race keeps
|
|
49663
|
-
* running; its late result is deliberately ignored, so a server reported as
|
|
49664
|
-
* timed out cannot quietly become connected afterwards -- status would then
|
|
49665
|
-
* tell doctor one thing and the registered tools another. */
|
|
49666
|
-
async connectOne(server, timeoutMs, onServer) {
|
|
49667
|
-
let timer;
|
|
49668
|
-
const deadline = new Promise((resolve14) => {
|
|
49669
|
-
timer = setTimeout(() => resolve14("timeout"), timeoutMs);
|
|
50608
|
+
try {
|
|
50609
|
+
const redirectUri = await uriP;
|
|
50610
|
+
const url = authorizeUrl({
|
|
50611
|
+
issuer: deps.config.issuer,
|
|
50612
|
+
clientId: deps.config.clientId,
|
|
50613
|
+
redirectUri,
|
|
50614
|
+
pkce,
|
|
50615
|
+
...deps.config.scope ? { scope: deps.config.scope } : {}
|
|
49670
50616
|
});
|
|
49671
|
-
|
|
49672
|
-
|
|
49673
|
-
|
|
49674
|
-
|
|
49675
|
-
|
|
49676
|
-
|
|
49677
|
-
|
|
50617
|
+
log("Opening your browser to sign in...");
|
|
50618
|
+
log(url);
|
|
50619
|
+
await deps.openBrowser(url);
|
|
50620
|
+
const { code: code2 } = await result;
|
|
50621
|
+
const tokens = await deps.exchange({
|
|
50622
|
+
issuer: deps.config.issuer,
|
|
50623
|
+
clientId: deps.config.clientId,
|
|
50624
|
+
code: code2,
|
|
50625
|
+
verifier: pkce.verifier,
|
|
50626
|
+
redirectUri
|
|
49678
50627
|
});
|
|
49679
|
-
|
|
49680
|
-
|
|
49681
|
-
|
|
49682
|
-
|
|
49683
|
-
}
|
|
49684
|
-
|
|
49685
|
-
|
|
49686
|
-
|
|
49687
|
-
|
|
49688
|
-
|
|
49689
|
-
|
|
49690
|
-
|
|
49691
|
-
|
|
49692
|
-
|
|
49693
|
-
|
|
49694
|
-
clearTimeout(timer);
|
|
49695
|
-
}
|
|
49696
|
-
const settled = this.statuses.get(server.name);
|
|
49697
|
-
if (settled) onServer?.({ server: server.name, ...settled });
|
|
49698
|
-
}
|
|
49699
|
-
/** Every tool from every connected server, paired with the server it came
|
|
49700
|
-
* from, in CONFIGURED order (`serverOrder`) -- not the order servers
|
|
49701
|
-
* happened to finish connecting in. Within a server, tools stay in the
|
|
49702
|
-
* order that server returned them. A server with no entry in `connected`
|
|
49703
|
-
* either failed to connect or reported zero tools -- `status()` tells the
|
|
49704
|
-
* two apart. */
|
|
49705
|
-
tools() {
|
|
49706
|
-
const out = [];
|
|
49707
|
-
for (const server of this.serverOrder) {
|
|
49708
|
-
const entry = this.connected.get(server);
|
|
49709
|
-
if (!entry) continue;
|
|
49710
|
-
for (const tool of entry.tools) out.push({ server, tool });
|
|
49711
|
-
}
|
|
49712
|
-
return out;
|
|
49713
|
-
}
|
|
49714
|
-
/**
|
|
49715
|
-
* Call one tool on one server. Fails immediately, naming the server, when
|
|
49716
|
-
* that server never connected -- there is nothing to route the call
|
|
49717
|
-
* through, and pretending otherwise would either stall waiting on a
|
|
49718
|
-
* connection that will never appear or return an empty result the caller
|
|
49719
|
-
* could mistake for a genuine (if empty) tool response.
|
|
49720
|
-
*/
|
|
49721
|
-
async call(server, tool, args) {
|
|
49722
|
-
const entry = this.connected.get(server);
|
|
49723
|
-
if (!entry) {
|
|
49724
|
-
throw new Error(`MCP server "${server}" is not connected -- run: corenel mcp doctor`);
|
|
49725
|
-
}
|
|
49726
|
-
return entry.client.callTool(tool, args);
|
|
49727
|
-
}
|
|
49728
|
-
/** Per-server connection state, for `doctor` (Task 10) to print. A server
|
|
49729
|
-
* with `connected: false` always carries `error` -- there is no code path
|
|
49730
|
-
* that records a failure without a reason, because "no tools" alone tells
|
|
49731
|
-
* the reader nothing about what to fix. */
|
|
49732
|
-
status() {
|
|
49733
|
-
const out = [];
|
|
49734
|
-
for (const server of this.serverOrder) {
|
|
49735
|
-
const s2 = this.statuses.get(server);
|
|
49736
|
-
if (s2) out.push({ server, ...s2 });
|
|
49737
|
-
}
|
|
49738
|
-
return out;
|
|
50628
|
+
if (!tokens.access_token) throw new Error("the token exchange returned no access token");
|
|
50629
|
+
const cred = {
|
|
50630
|
+
accessToken: tokens.access_token,
|
|
50631
|
+
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : {},
|
|
50632
|
+
...typeof tokens.expires_in === "number" ? { expiresAt: now2() + tokens.expires_in * 1e3 } : {},
|
|
50633
|
+
...tokens.account ? { account: tokens.account } : {},
|
|
50634
|
+
...deps.apiBase ? { apiBase: deps.apiBase } : {}
|
|
50635
|
+
};
|
|
50636
|
+
await saveCredential(cred, deps.stateDir);
|
|
50637
|
+
return {
|
|
50638
|
+
...cred.account ? { account: cred.account } : {},
|
|
50639
|
+
...cred.expiresAt ? { expiresAt: cred.expiresAt } : {}
|
|
50640
|
+
};
|
|
50641
|
+
} finally {
|
|
50642
|
+
close2();
|
|
49739
50643
|
}
|
|
49740
|
-
}
|
|
49741
|
-
|
|
49742
|
-
// src/mcp/toolNames.ts
|
|
49743
|
-
init_protocol();
|
|
50644
|
+
}
|
|
49744
50645
|
|
|
49745
|
-
// src/
|
|
49746
|
-
|
|
49747
|
-
|
|
50646
|
+
// src/auth/cli.ts
|
|
50647
|
+
init_tools_node();
|
|
50648
|
+
function oauthConfigFromEnv(env = process.env) {
|
|
50649
|
+
const issuer = env.CORENEL_OAUTH_ISSUER?.trim();
|
|
50650
|
+
const clientId = env.CORENEL_OAUTH_CLIENT_ID?.trim();
|
|
50651
|
+
if (!issuer || !clientId) return null;
|
|
50652
|
+
return { issuer, clientId };
|
|
49748
50653
|
}
|
|
49749
|
-
function
|
|
49750
|
-
|
|
49751
|
-
|
|
50654
|
+
async function exchangeCode2(p2) {
|
|
50655
|
+
const res = await fetch(new URL("/oauth/token", p2.issuer), {
|
|
50656
|
+
method: "POST",
|
|
50657
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
50658
|
+
body: new URLSearchParams({
|
|
50659
|
+
grant_type: "authorization_code",
|
|
50660
|
+
client_id: p2.clientId,
|
|
50661
|
+
code: p2.code,
|
|
50662
|
+
code_verifier: p2.verifier,
|
|
50663
|
+
redirect_uri: p2.redirectUri
|
|
50664
|
+
}).toString()
|
|
50665
|
+
});
|
|
50666
|
+
if (!res.ok) {
|
|
50667
|
+
throw new Error(`token exchange failed (HTTP ${res.status})`);
|
|
50668
|
+
}
|
|
50669
|
+
return await res.json();
|
|
49752
50670
|
}
|
|
49753
|
-
function
|
|
49754
|
-
|
|
49755
|
-
if (
|
|
49756
|
-
|
|
49757
|
-
|
|
49758
|
-
|
|
50671
|
+
async function runLogin(stateDir, apiBase2) {
|
|
50672
|
+
const config = oauthConfigFromEnv();
|
|
50673
|
+
if (!config) {
|
|
50674
|
+
console.error("Sign-in is not configured on this build.");
|
|
50675
|
+
console.error("Set CORENEL_OAUTH_ISSUER and CORENEL_OAUTH_CLIENT_ID, or use " + TOKEN_ENV2 + " for headless use.");
|
|
50676
|
+
return 1;
|
|
50677
|
+
}
|
|
50678
|
+
try {
|
|
50679
|
+
const res = await login({
|
|
50680
|
+
config,
|
|
50681
|
+
openBrowser,
|
|
50682
|
+
exchange: exchangeCode2,
|
|
50683
|
+
log: (l2) => console.error(l2),
|
|
50684
|
+
...stateDir ? { stateDir } : {},
|
|
50685
|
+
// Recorded so the daemon does not have to guess later which API this
|
|
50686
|
+
// credential belongs to -- see LoginDeps.apiBase.
|
|
50687
|
+
...apiBase2 ? { apiBase: apiBase2 } : {}
|
|
49759
50688
|
});
|
|
49760
|
-
|
|
50689
|
+
console.error(`Signed in${res.account ? ` as ${res.account}` : ""}.`);
|
|
50690
|
+
return 0;
|
|
50691
|
+
} catch (err) {
|
|
50692
|
+
console.error(`Sign-in failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
50693
|
+
return 1;
|
|
49761
50694
|
}
|
|
49762
|
-
if (content == null) return "";
|
|
49763
|
-
return JSON.stringify(content);
|
|
49764
50695
|
}
|
|
49765
|
-
function
|
|
49766
|
-
const
|
|
49767
|
-
|
|
49768
|
-
|
|
49769
|
-
|
|
49770
|
-
|
|
49771
|
-
|
|
49772
|
-
|
|
49773
|
-
}
|
|
49774
|
-
out.push(toAgentTool(registry2, server, tool));
|
|
50696
|
+
async function runLogout(stateDir, env = process.env) {
|
|
50697
|
+
const before = await resolveCredential(env, stateDir);
|
|
50698
|
+
await clearCredential(stateDir);
|
|
50699
|
+
console.error("Signed out. (Paired devices are unaffected - run `hosts forget` to unpair.)");
|
|
50700
|
+
console.error(` Cleared ${tokenFilePath(stateDir)} and ${credentialFilePath(stateDir)}`);
|
|
50701
|
+
if (before.source === "none") console.error(" Nothing was stored - this machine was already signed out.");
|
|
50702
|
+
if (env[TOKEN_ENV2]?.trim()) {
|
|
50703
|
+
console.error(` NOTE: ${TOKEN_ENV2} is still set in this environment, so this machine is STILL signed in.`);
|
|
49775
50704
|
}
|
|
49776
|
-
return
|
|
50705
|
+
return 0;
|
|
49777
50706
|
}
|
|
49778
|
-
function
|
|
49779
|
-
|
|
49780
|
-
const address = mcpToolAddress(server, tool.name);
|
|
49781
|
-
return {
|
|
49782
|
-
name,
|
|
49783
|
-
namespace: "mcp",
|
|
49784
|
-
description: tool.description?.trim() || `MCP tool "${tool.name}" on server "${server}" (${address}).`,
|
|
49785
|
-
parameters: toParameters(tool.inputSchema),
|
|
49786
|
-
mutates: true,
|
|
49787
|
-
async run(args) {
|
|
49788
|
-
const result = await registry2.call(server, tool.name, args);
|
|
49789
|
-
const text = formatContent(result.content);
|
|
49790
|
-
return result.isError ? `Error: ${text}` : text;
|
|
49791
|
-
}
|
|
49792
|
-
};
|
|
50707
|
+
function sourceLabel(source) {
|
|
50708
|
+
return source === "env" ? `${TOKEN_ENV2} environment variable` : source === "token-file" ? "token file" : source === "credential-file" ? "credential file (auth.json)" : "none";
|
|
49793
50709
|
}
|
|
49794
|
-
|
|
49795
|
-
|
|
49796
|
-
|
|
49797
|
-
|
|
49798
|
-
|
|
49799
|
-
|
|
49800
|
-
|
|
49801
|
-
const service = new PermissionService({
|
|
49802
|
-
policy,
|
|
49803
|
-
defaultForReadOnly: "allow",
|
|
49804
|
-
defaultForMutating: "allow"
|
|
49805
|
-
});
|
|
49806
|
-
const level = service.resolveLevel({ name: address, address, mutates: true });
|
|
49807
|
-
if (level !== "allow") {
|
|
49808
|
-
return {
|
|
49809
|
-
ok: false,
|
|
49810
|
-
reason: `${address} resolves to permission "${level}" - MCP tools never prompt; set it to "allow" or remove the rule`
|
|
49811
|
-
};
|
|
50710
|
+
async function runAuthStatus(stateDir, env = process.env) {
|
|
50711
|
+
const r2 = await resolveCredential(env, stateDir);
|
|
50712
|
+
if (!r2.token) {
|
|
50713
|
+
console.error("Not signed in. Run `corenel login`, or set " + TOKEN_ENV2 + ".");
|
|
50714
|
+
console.error(` Looked in ${tokenFilePath(stateDir)}`);
|
|
50715
|
+
console.error(` Looked in ${credentialFilePath(stateDir)}`);
|
|
50716
|
+
return 1;
|
|
49812
50717
|
}
|
|
49813
|
-
|
|
50718
|
+
console.error(`Signed in (${r2.source === "env" ? sourceLabel(r2.source) : r2.path}).`);
|
|
50719
|
+
console.error(` account ${r2.account ?? "not recorded in this store - run `corenel login` to record it"}`);
|
|
50720
|
+
console.error(` token ${maskToken(r2.token)}`);
|
|
50721
|
+
console.error(` source ${sourceLabel(r2.source)}`);
|
|
50722
|
+
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"}`);
|
|
50723
|
+
return r2.expired ? 1 : 0;
|
|
49814
50724
|
}
|
|
49815
50725
|
|
|
50726
|
+
// src/cli.ts
|
|
50727
|
+
init_fileService();
|
|
50728
|
+
import { relative as relative7, resolve as resolvePath } from "node:path";
|
|
50729
|
+
|
|
49816
50730
|
// src/mcp/daemonTools.ts
|
|
49817
50731
|
function buildMcpPolicy(fileConfig) {
|
|
49818
50732
|
return {
|
|
@@ -49824,24 +50738,7 @@ function buildMcpPolicy(fileConfig) {
|
|
|
49824
50738
|
};
|
|
49825
50739
|
}
|
|
49826
50740
|
function registerMcpTools(registry2, policy) {
|
|
49827
|
-
const
|
|
49828
|
-
const byWireName = new Map(base.map((t2) => [t2.name, t2]));
|
|
49829
|
-
const wrapped = [];
|
|
49830
|
-
for (const { server, tool } of registry2.tools()) {
|
|
49831
|
-
if (!canEncodeMcpWireName(server, tool.name)) continue;
|
|
49832
|
-
const name = mcpWireName(server, tool.name);
|
|
49833
|
-
const original = byWireName.get(name);
|
|
49834
|
-
if (!original) continue;
|
|
49835
|
-
const address = mcpToolAddress(server, tool.name);
|
|
49836
|
-
wrapped.push({
|
|
49837
|
-
...original,
|
|
49838
|
-
async run(args, ctx) {
|
|
49839
|
-
const decision = mcpCallAllowed(address, policy);
|
|
49840
|
-
if (!decision.ok) return `Error: ${decision.reason}`;
|
|
49841
|
-
return original.run(args, ctx);
|
|
49842
|
-
}
|
|
49843
|
-
});
|
|
49844
|
-
}
|
|
50741
|
+
const wrapped = gatedAgentTools(registry2, policy);
|
|
49845
50742
|
for (const tool of wrapped) registerTool(tool);
|
|
49846
50743
|
return () => {
|
|
49847
50744
|
for (const tool of wrapped) unregisterTool(tool.name);
|