bazilion 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1666 -266
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +4303 -522
- package/dist/daemon.js.map +1 -1
- package/dist/migrations/0001_init.sql +101 -0
- package/dist/web/client/assets/AgentTabs-R6gSjIj3.js +1 -0
- package/dist/web/client/assets/{ProductionTeamPolicyEditor-CiQ-VVDo.js → ProductionTeamPolicyEditor-CSL3W7XM.js} +2 -2
- package/dist/web/client/assets/{ProfileCommunicationEditor-D84F5yfr.js → ProfileCommunicationEditor-CwU0QkmE.js} +1 -1
- package/dist/web/client/assets/{TeamTabs-Btoe_FAE.js → TeamTabs-C1Hakkff.js} +1 -1
- package/dist/web/client/assets/{TemplatesTabs-CAYY03F5.js → TemplatesTabs-klEydJ8-.js} +1 -1
- package/dist/web/client/assets/{_id-BqU2BodK.js → _id-BOC7ioJu.js} +1 -1
- package/dist/web/client/assets/{_id-CKtoio3t.js → _id-CnVq-rED.js} +1 -1
- package/dist/web/client/assets/{_id-BeauECmk.js → _id-e4u7dmBb.js} +1 -1
- package/dist/web/client/assets/{_id-Q98Of6k9.js → _id-wkjjWLz0.js} +1 -1
- package/dist/web/client/assets/{activity-DXN_N85w.js → activity-uqhwzmiy.js} +1 -1
- package/dist/web/client/assets/{agents-C5Xsdge1.js → agents-B2usE-L_.js} +1 -1
- package/dist/web/client/assets/{agents-Cgxo4H5G.js → agents-FaQtkAWs.js} +1 -1
- package/dist/web/client/assets/{approvals-BnG7fbw-.js → approvals-DqLE6ttw.js} +1 -1
- package/dist/web/client/assets/attention-CkGozYnJ.js +1 -0
- package/dist/web/client/assets/attention-Gavt2yEp.js +1 -0
- package/dist/web/client/assets/chevron-down-CJk0zGNT.js +1 -0
- package/dist/web/client/assets/{config-xw4NyrXF.js → config-xrXoDEWM.js} +1 -1
- package/dist/web/client/assets/{context-D2V1Oi4z.js → context-ZWqx3uiC.js} +1 -1
- package/dist/web/client/assets/inbox-BoIvl9ZF.js +1 -0
- package/dist/web/client/assets/{index-VdURSoM4.js → index-nb2MV8X1.js} +3 -3
- package/dist/web/client/assets/learning-6_oYn1l7.js +1 -0
- package/dist/web/client/assets/{login-CvptUoMR.js → login-DM1ytxQL.js} +1 -1
- package/dist/web/client/assets/{mcp-BDFM_sW-.js → mcp-CetZbyUh.js} +1 -1
- package/dist/web/client/assets/{members-BJvph_TR.js → members-DdqWtwZd.js} +1 -1
- package/dist/web/client/assets/{memory-DRytqZCD.js → memory-FNrxFHmC.js} +1 -1
- package/dist/web/client/assets/plus-C3GroITC.js +1 -0
- package/dist/web/client/assets/{policy-CGP50OtQ.js → policy-BKPejYRI.js} +1 -1
- package/dist/web/client/assets/{routes-DJnWYvC9.js → routes-CH1ZTTo-.js} +1 -1
- package/dist/web/client/assets/{services-DHheiv7L.js → services-zk9SqN06.js} +1 -1
- package/dist/web/client/assets/{skills-DmsitluU.js → skills-tXDpomHW.js} +1 -1
- package/dist/web/client/assets/styles-DMltY3su.css +2 -0
- package/dist/web/client/assets/{teams-BdC4gthq.js → teams-BZYjekBN.js} +1 -1
- package/dist/web/client/assets/{teams-CbxSaorm.js → teams-D1c8zlxs.js} +1 -1
- package/dist/web/client/assets/telegram-DCe1CWMt.js +1 -0
- package/dist/web/client/assets/{tokens-CjRplDrJ.js → tokens-DAPjRS0N.js} +1 -1
- package/dist/web/client/assets/triggers-D8R-5guA.js +1 -0
- package/dist/web/client/assets/{welcome-wBfX9dAd.js → welcome-CuK0shDJ.js} +1 -1
- package/dist/web/client/assets/wire-constants-BhVKOCQa.js +74 -0
- package/dist/web/server/assets/{AgentTabs-DoMDJBXH.js → AgentTabs-aXdsiiMp.js} +5 -0
- package/dist/web/server/assets/{ChatPane-eUY_dga3.js → ChatPane-Ba5wTsVk.js} +325 -36
- package/dist/web/server/assets/{_id-ClN9hY2x.js → _id-BHYLL8M-.js} +3 -3
- package/dist/web/server/assets/{_id-DkYodSkG.js → _id-FcHdSocl.js} +1 -1
- package/dist/web/server/assets/{_tanstack-start-manifest_v-D09Xu_6u.js → _tanstack-start-manifest_v-fLsYERQ-.js} +77 -54
- package/dist/web/server/assets/attention-BW_6ddkj.js +11 -0
- package/dist/web/server/assets/attention-BxkTf337.js +14 -0
- package/dist/web/server/assets/attention-CM-Ks1N9.js +19 -0
- package/dist/web/server/assets/attention-w21GQIGH.js +264 -0
- package/dist/web/server/assets/{inbox-CUIyf9MT.js → inbox-BLUzA5sQ.js} +40 -3
- package/dist/web/server/assets/{inbox-B3-TRsNI.js → inbox-CzlKT9Wg.js} +5 -3
- package/dist/web/server/assets/{inbox-BardsGyy.js → inbox-ZvDNAjaM.js} +1 -1
- package/dist/web/server/assets/learning-8zav4pIT.js +33 -0
- package/dist/web/server/assets/learning-B7PBkvER.js +16 -0
- package/dist/web/server/assets/learning-Cq7K2heb.js +312 -0
- package/dist/web/server/assets/{router-DIdp8ZNt.js → router-CQuj7553.js} +81 -48
- package/dist/web/server/assets/{routes-Jb0zjKav.js → routes-B6cMLIPk.js} +3 -3
- package/dist/web/server/assets/{routes-CXz4hTAr.js → routes-BrA-U_Hm.js} +1 -1
- package/dist/web/server/assets/{routes-lIclH9Br.js → routes-C19CMLZt.js} +1 -1
- package/dist/web/server/assets/{telegram-7ub--6xz.js → telegram-CBF8SJzB.js} +1 -24
- package/dist/web/server/assets/{telegram-Bazhmpr4.js → telegram-XwyKSEtj.js} +1 -1
- package/dist/web/server/assets/{triggers-Ca57_qym.js → triggers-BZPl5CHs.js} +1 -1
- package/dist/web/server/assets/{triggers-BMpHsGIr.js → triggers-CSNNzRIi.js} +28 -4
- package/dist/web/server/assets/{triggers-C_0y6QV9.js → triggers-D79NoSXk.js} +3 -2
- package/dist/web/server/server.js +13 -5
- package/dist/worker.js +1752 -148
- package/dist/worker.js.map +1 -1
- package/package.json +4 -3
- package/dist/web/client/assets/AgentTabs-B3hq5a3e.js +0 -1
- package/dist/web/client/assets/chevron-down-BhcBspw3.js +0 -1
- package/dist/web/client/assets/inbox-DK6hWCMp.js +0 -1
- package/dist/web/client/assets/plus-C9ee3SIO.js +0 -1
- package/dist/web/client/assets/styles-B5eQAwLP.css +0 -2
- package/dist/web/client/assets/telegram-D61G_alO.js +0 -1
- package/dist/web/client/assets/triggers-MLzTm_DM.js +0 -1
- package/dist/web/client/assets/wire-constants-BxfyUMXB.js +0 -74
- package/dist/web/server/assets/{Sidebar-O_CH1-JD.js → Sidebar-DsRVCszF.js} +1 -1
package/dist/worker.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// ../daemon/src/runtime/worker/entry.ts
|
|
4
|
-
import { randomUUID as
|
|
5
|
-
import {
|
|
4
|
+
import { randomUUID as randomUUID19 } from "crypto";
|
|
5
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync12 } from "fs";
|
|
6
|
+
import { join as join19 } from "path";
|
|
7
|
+
import { Type as Type3 } from "typebox";
|
|
6
8
|
|
|
7
9
|
// ../daemon/src/core/agent/delete.ts
|
|
8
10
|
import { randomUUID } from "crypto";
|
|
@@ -18,8 +20,11 @@ import { join as join2 } from "path";
|
|
|
18
20
|
// ../daemon/src/core/profile/identity.ts
|
|
19
21
|
import { readFileSync as readFileSync2 } from "fs";
|
|
20
22
|
|
|
21
|
-
// ../daemon/src/core/
|
|
23
|
+
// ../daemon/src/core/repos/agentLessonProposals.ts
|
|
22
24
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
25
|
+
|
|
26
|
+
// ../daemon/src/core/agent/spawn.ts
|
|
27
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
23
28
|
import { mkdirSync as mkdirSync3, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
|
|
24
29
|
import { join as join7 } from "path";
|
|
25
30
|
|
|
@@ -44,7 +49,7 @@ import { existsSync as existsSync5, readdirSync } from "fs";
|
|
|
44
49
|
import { join as join6 } from "path";
|
|
45
50
|
|
|
46
51
|
// ../daemon/src/core/db/client.ts
|
|
47
|
-
import { DatabaseSync } from "node:sqlite";
|
|
52
|
+
import { backup, DatabaseSync } from "node:sqlite";
|
|
48
53
|
|
|
49
54
|
// ../daemon/src/core/db/migrate.ts
|
|
50
55
|
import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
|
|
@@ -85,14 +90,17 @@ function resolvePaths(home) {
|
|
|
85
90
|
import { existsSync as existsSync6, rmSync as rmSync4 } from "fs";
|
|
86
91
|
|
|
87
92
|
// ../daemon/src/core/repos/teamTemplates.ts
|
|
88
|
-
import { randomUUID as
|
|
93
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
89
94
|
|
|
90
95
|
// ../daemon/src/core/profile/update.ts
|
|
91
96
|
import { writeFileSync as writeFileSync5 } from "fs";
|
|
92
97
|
import { join as join10 } from "path";
|
|
93
98
|
|
|
99
|
+
// ../daemon/src/core/repos/agentReviews.ts
|
|
100
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
101
|
+
|
|
94
102
|
// ../daemon/src/core/repos/communicationApprovals.ts
|
|
95
|
-
import { createHash, randomUUID as
|
|
103
|
+
import { createHash, randomUUID as randomUUID6 } from "crypto";
|
|
96
104
|
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
97
105
|
|
|
98
106
|
// ../daemon/src/core/services.ts
|
|
@@ -495,6 +503,50 @@ var SERVICES = [
|
|
|
495
503
|
}
|
|
496
504
|
]
|
|
497
505
|
},
|
|
506
|
+
{
|
|
507
|
+
id: "shell-security",
|
|
508
|
+
displayName: "Agent Shell Security",
|
|
509
|
+
category: "service",
|
|
510
|
+
team: "Agent Runtime",
|
|
511
|
+
hint: "Optional dangerous-command approval and Docker isolation. Disabled by default.",
|
|
512
|
+
fields: [
|
|
513
|
+
{
|
|
514
|
+
envVar: "BAZILION_BASH_APPROVAL",
|
|
515
|
+
kind: "config",
|
|
516
|
+
label: "Dangerous-command approval",
|
|
517
|
+
placeholder: "off",
|
|
518
|
+
description: "Use off (current behavior) or dangerous (classified bash commands require per-call approval before execution)."
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
envVar: "BAZILION_BASH_SANDBOX",
|
|
522
|
+
kind: "config",
|
|
523
|
+
label: "Sandbox mode",
|
|
524
|
+
placeholder: "off",
|
|
525
|
+
description: "Use off (current host tools) or docker (containerized bash only; host-backed coding tools are hidden)."
|
|
526
|
+
},
|
|
527
|
+
{
|
|
528
|
+
envVar: "BAZILION_BASH_SANDBOX_IMAGE",
|
|
529
|
+
kind: "config",
|
|
530
|
+
label: "Docker image",
|
|
531
|
+
placeholder: "debian:bookworm-slim",
|
|
532
|
+
description: "Local image with /bin/bash and /usr/bin/env and no VOLUME declarations. Bazilion never falls back to host execution if it is unavailable or incompatible."
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
envVar: "BAZILION_BASH_SANDBOX_ENV_ALLOWLIST",
|
|
536
|
+
kind: "config",
|
|
537
|
+
label: "Extra environment allowlist",
|
|
538
|
+
placeholder: "MY_SAFE_VARIABLE,ANOTHER_SAFE_VARIABLE",
|
|
539
|
+
description: "Optional comma-separated variable names copied into the container. Provider credentials remain excluded unless explicitly named."
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
envVar: "BAZILION_AGENT_LOOP_MAX_HOPS",
|
|
543
|
+
kind: "config",
|
|
544
|
+
label: "Agent-message hop limit",
|
|
545
|
+
placeholder: "8",
|
|
546
|
+
description: "Maximum causal hops in an agent-to-agent message chain before the daemon stops it. Default 8."
|
|
547
|
+
}
|
|
548
|
+
]
|
|
549
|
+
},
|
|
498
550
|
{
|
|
499
551
|
id: "browser",
|
|
500
552
|
displayName: "Browser Automation",
|
|
@@ -597,19 +649,22 @@ var CONFIG_KEYS = [
|
|
|
597
649
|
var CONFIG_KEY_SET = new Set(CONFIG_KEYS);
|
|
598
650
|
|
|
599
651
|
// ../daemon/src/core/repos/mcpServers.ts
|
|
600
|
-
import { randomUUID as
|
|
652
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
601
653
|
|
|
602
654
|
// ../daemon/src/core/repos/messages.ts
|
|
603
|
-
import { randomUUID as
|
|
655
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
604
656
|
|
|
605
657
|
// ../daemon/src/core/repos/secrets.ts
|
|
606
658
|
import { createCipheriv, createDecipheriv, pbkdf2Sync, randomBytes } from "crypto";
|
|
607
659
|
|
|
660
|
+
// ../daemon/src/core/repos/triggerDispatches.ts
|
|
661
|
+
import { randomUUID as randomUUID9 } from "crypto";
|
|
662
|
+
|
|
608
663
|
// ../daemon/src/core/repos/triggers.ts
|
|
609
|
-
import { randomUUID as
|
|
664
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
610
665
|
|
|
611
666
|
// ../daemon/src/core/repos/webTokens.ts
|
|
612
|
-
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as
|
|
667
|
+
import { createHash as createHash2, randomBytes as randomBytes2, randomUUID as randomUUID11 } from "crypto";
|
|
613
668
|
|
|
614
669
|
// ../daemon/src/core/secrets.ts
|
|
615
670
|
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
@@ -623,22 +678,52 @@ import AdmZip from "adm-zip";
|
|
|
623
678
|
// ../daemon/src/core/skills/parse.ts
|
|
624
679
|
import { readFileSync as readFileSync7 } from "fs";
|
|
625
680
|
import { parse as parseYaml } from "yaml";
|
|
681
|
+
var FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
|
|
682
|
+
function parseSkillContent(raw) {
|
|
683
|
+
const m = raw.match(FRONTMATTER_RE);
|
|
684
|
+
if (!m) {
|
|
685
|
+
throw new Error('SKILL.md missing YAML frontmatter (expected leading "---")');
|
|
686
|
+
}
|
|
687
|
+
const yamlBlock = m[1] ?? "";
|
|
688
|
+
const body = m[2] ?? "";
|
|
689
|
+
let fm;
|
|
690
|
+
try {
|
|
691
|
+
fm = parseYaml(yamlBlock);
|
|
692
|
+
} catch (err) {
|
|
693
|
+
throw new Error(`SKILL.md frontmatter is not valid YAML: ${err.message}`);
|
|
694
|
+
}
|
|
695
|
+
if (!fm || typeof fm !== "object" || Array.isArray(fm)) {
|
|
696
|
+
throw new Error("SKILL.md frontmatter must be a YAML object");
|
|
697
|
+
}
|
|
698
|
+
const fmObj = fm;
|
|
699
|
+
if (typeof fmObj.name !== "string" || fmObj.name.length === 0) {
|
|
700
|
+
throw new Error('SKILL.md frontmatter missing required "name"');
|
|
701
|
+
}
|
|
702
|
+
if (typeof fmObj.description !== "string" || fmObj.description.length === 0) {
|
|
703
|
+
throw new Error('SKILL.md frontmatter missing required "description"');
|
|
704
|
+
}
|
|
705
|
+
return { frontmatter: fmObj, body, raw };
|
|
706
|
+
}
|
|
707
|
+
function parseSkillFile(path) {
|
|
708
|
+
const raw = readFileSync7(path, "utf8");
|
|
709
|
+
return parseSkillContent(raw);
|
|
710
|
+
}
|
|
626
711
|
|
|
627
712
|
// ../daemon/src/core/team/delete.ts
|
|
628
|
-
import { randomUUID as
|
|
713
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
629
714
|
import { existsSync as existsSync9, renameSync as renameSync2, rmSync as rmSync6 } from "fs";
|
|
630
715
|
|
|
631
716
|
// ../daemon/src/core/team-policy/adopt.ts
|
|
632
|
-
import { randomUUID as
|
|
717
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
633
718
|
|
|
634
719
|
// ../daemon/src/core/team-policy/authorization.ts
|
|
635
|
-
import { createHash as createHash3, randomUUID as
|
|
720
|
+
import { createHash as createHash3, randomUUID as randomUUID14 } from "crypto";
|
|
636
721
|
|
|
637
722
|
// ../daemon/src/core/team-policy/source.ts
|
|
638
|
-
import { randomUUID as
|
|
723
|
+
import { randomUUID as randomUUID15 } from "crypto";
|
|
639
724
|
|
|
640
725
|
// ../daemon/src/core/team-policy/spawn.ts
|
|
641
|
-
import { randomUUID as
|
|
726
|
+
import { randomUUID as randomUUID16 } from "crypto";
|
|
642
727
|
import { readdirSync as readdirSync4, rmSync as rmSync8 } from "fs";
|
|
643
728
|
|
|
644
729
|
// ../daemon/src/core/team-policy/rm-with-retry.ts
|
|
@@ -646,15 +731,19 @@ import { rmSync as rmSync7 } from "fs";
|
|
|
646
731
|
|
|
647
732
|
// ../daemon/src/runtime/memory/qmd.ts
|
|
648
733
|
import {
|
|
649
|
-
|
|
734
|
+
closeSync,
|
|
735
|
+
constants,
|
|
736
|
+
fstatSync,
|
|
737
|
+
lstatSync,
|
|
650
738
|
mkdirSync as mkdirSync4,
|
|
739
|
+
openSync,
|
|
651
740
|
readdirSync as readdirSync5,
|
|
652
741
|
readFileSync as readFileSync8,
|
|
742
|
+
realpathSync,
|
|
653
743
|
rmSync as rmSync9,
|
|
654
|
-
statSync as statSync3,
|
|
655
744
|
writeFileSync as writeFileSync6
|
|
656
745
|
} from "fs";
|
|
657
|
-
import { dirname as dirname2, join as join12 } from "path";
|
|
746
|
+
import { dirname as dirname2, isAbsolute, join as join12, relative, resolve as resolve3, sep as sep2 } from "path";
|
|
658
747
|
import { createStore, extractSnippet } from "@tobilu/qmd";
|
|
659
748
|
var INDEX_FILENAME = ".qmd-index.sqlite";
|
|
660
749
|
var COLLECTION_NAME = "memory";
|
|
@@ -675,61 +764,222 @@ function getStore(dir) {
|
|
|
675
764
|
}
|
|
676
765
|
return p;
|
|
677
766
|
}
|
|
767
|
+
function unsafeMemoryPath(path, reason) {
|
|
768
|
+
return new Error(`unsafe memory path "${path}": ${reason}`);
|
|
769
|
+
}
|
|
770
|
+
function lstatIfPresent(path) {
|
|
771
|
+
try {
|
|
772
|
+
return lstatSync(path);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
if (error.code === "ENOENT") return void 0;
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function isContained(root, candidate) {
|
|
779
|
+
const rel = relative(root, candidate);
|
|
780
|
+
return rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep2}`);
|
|
781
|
+
}
|
|
782
|
+
function assertContained(root, candidate) {
|
|
783
|
+
if (!isContained(root, candidate)) {
|
|
784
|
+
throw unsafeMemoryPath(candidate, `resolved outside memory root "${root}"`);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
function initializeSafeRoot(root) {
|
|
788
|
+
mkdirSync4(root, { recursive: true });
|
|
789
|
+
const info = lstatSync(root);
|
|
790
|
+
if (info.isSymbolicLink()) {
|
|
791
|
+
throw unsafeMemoryPath(root, "memory root must not be a symbolic link");
|
|
792
|
+
}
|
|
793
|
+
if (!info.isDirectory()) {
|
|
794
|
+
throw unsafeMemoryPath(root, "memory root is not a directory");
|
|
795
|
+
}
|
|
796
|
+
return realpathSync(root);
|
|
797
|
+
}
|
|
798
|
+
function assertRootUnchanged(root, canonicalRoot) {
|
|
799
|
+
const info = lstatIfPresent(root);
|
|
800
|
+
if (!info) throw unsafeMemoryPath(root, "memory root disappeared");
|
|
801
|
+
if (info.isSymbolicLink()) {
|
|
802
|
+
throw unsafeMemoryPath(root, "memory root must not be a symbolic link");
|
|
803
|
+
}
|
|
804
|
+
if (!info.isDirectory()) {
|
|
805
|
+
throw unsafeMemoryPath(root, "memory root is not a directory");
|
|
806
|
+
}
|
|
807
|
+
const current = realpathSync(root);
|
|
808
|
+
if (relative(canonicalRoot, current) !== "" || relative(current, canonicalRoot) !== "") {
|
|
809
|
+
throw unsafeMemoryPath(root, "memory root changed after initialization");
|
|
810
|
+
}
|
|
811
|
+
}
|
|
678
812
|
function safeKey(root, key) {
|
|
679
|
-
|
|
813
|
+
const segments = key.split(/[\\/]/);
|
|
814
|
+
if (key.length === 0 || key.includes("\0") || isAbsolute(key) || /^[A-Za-z]:[\\/]/.test(key) || key.startsWith("\\\\") || segments.includes("..")) {
|
|
680
815
|
throw new Error(`unsafe memory key: ${key}`);
|
|
681
816
|
}
|
|
682
|
-
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
817
|
+
const candidate = resolve3(root, key);
|
|
818
|
+
if (candidate === root) throw new Error(`unsafe memory key: ${key}`);
|
|
819
|
+
assertContained(root, candidate);
|
|
820
|
+
return candidate;
|
|
821
|
+
}
|
|
822
|
+
function inspectSafePath(root, path) {
|
|
823
|
+
assertContained(root, path);
|
|
824
|
+
const rel = relative(root, path);
|
|
825
|
+
let current = root;
|
|
826
|
+
const components = rel.split(sep2).filter(Boolean);
|
|
827
|
+
for (let index = 0; index < components.length; index += 1) {
|
|
828
|
+
current = join12(current, components[index]);
|
|
829
|
+
const info = lstatIfPresent(current);
|
|
830
|
+
if (!info) return void 0;
|
|
831
|
+
if (info.isSymbolicLink()) {
|
|
832
|
+
throw unsafeMemoryPath(current, "symbolic links are not allowed");
|
|
833
|
+
}
|
|
834
|
+
if (index < components.length - 1 && !info.isDirectory()) {
|
|
835
|
+
throw unsafeMemoryPath(current, "path ancestor is not a directory");
|
|
836
|
+
}
|
|
837
|
+
assertContained(root, realpathSync(current));
|
|
838
|
+
}
|
|
839
|
+
return lstatIfPresent(path);
|
|
840
|
+
}
|
|
841
|
+
function ensureSafeParentDirectories(root, path) {
|
|
842
|
+
const parent = dirname2(path);
|
|
843
|
+
assertContained(root, parent);
|
|
844
|
+
const rel = relative(root, parent);
|
|
845
|
+
let current = root;
|
|
846
|
+
for (const component of rel.split(sep2).filter(Boolean)) {
|
|
847
|
+
current = join12(current, component);
|
|
848
|
+
let info = lstatIfPresent(current);
|
|
849
|
+
if (!info) {
|
|
850
|
+
mkdirSync4(current);
|
|
851
|
+
info = lstatSync(current);
|
|
852
|
+
}
|
|
853
|
+
if (info.isSymbolicLink()) {
|
|
854
|
+
throw unsafeMemoryPath(current, "symbolic-link ancestors are not allowed");
|
|
855
|
+
}
|
|
856
|
+
if (!info.isDirectory()) {
|
|
857
|
+
throw unsafeMemoryPath(current, "path ancestor is not a directory");
|
|
858
|
+
}
|
|
859
|
+
assertContained(root, realpathSync(current));
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function readSafeFile(root, path) {
|
|
863
|
+
const info = inspectSafePath(root, path);
|
|
864
|
+
if (!info) throw new Error(`memory entry not found: ${relative(root, path)}`);
|
|
865
|
+
if (!info.isFile()) throw unsafeMemoryPath(path, "memory entry is not a regular file");
|
|
866
|
+
let fd;
|
|
867
|
+
try {
|
|
868
|
+
fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
869
|
+
const stats = fstatSync(fd);
|
|
870
|
+
if (!stats.isFile()) throw unsafeMemoryPath(path, "memory entry is not a regular file");
|
|
871
|
+
return { content: readFileSync8(fd, "utf8"), stats };
|
|
872
|
+
} catch (error) {
|
|
873
|
+
if (error.code === "ELOOP") {
|
|
874
|
+
throw unsafeMemoryPath(path, "symbolic links are not allowed");
|
|
875
|
+
}
|
|
876
|
+
throw error;
|
|
877
|
+
} finally {
|
|
878
|
+
if (fd !== void 0) closeSync(fd);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function writeSafeFile(root, path, content) {
|
|
882
|
+
ensureSafeParentDirectories(root, path);
|
|
883
|
+
const existing = inspectSafePath(root, path);
|
|
884
|
+
if (existing?.isSymbolicLink()) {
|
|
885
|
+
throw unsafeMemoryPath(path, "symbolic links are not allowed");
|
|
886
|
+
}
|
|
887
|
+
if (existing && !existing.isFile()) {
|
|
888
|
+
throw unsafeMemoryPath(path, "memory entry is not a regular file");
|
|
889
|
+
}
|
|
890
|
+
let fd;
|
|
891
|
+
try {
|
|
892
|
+
fd = openSync(
|
|
893
|
+
path,
|
|
894
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW,
|
|
895
|
+
438
|
|
896
|
+
);
|
|
897
|
+
const stats = fstatSync(fd);
|
|
898
|
+
if (!stats.isFile()) throw unsafeMemoryPath(path, "memory entry is not a regular file");
|
|
899
|
+
writeFileSync6(fd, content);
|
|
900
|
+
return fstatSync(fd);
|
|
901
|
+
} catch (error) {
|
|
902
|
+
if (error.code === "ELOOP") {
|
|
903
|
+
throw unsafeMemoryPath(path, "symbolic links are not allowed");
|
|
904
|
+
}
|
|
905
|
+
throw error;
|
|
906
|
+
} finally {
|
|
907
|
+
if (fd !== void 0) closeSync(fd);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
function assertSafeTree(root, dir = root) {
|
|
911
|
+
assertContained(root, realpathSync(dir));
|
|
912
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
913
|
+
const full = join12(dir, entry.name);
|
|
914
|
+
const info = lstatSync(full);
|
|
915
|
+
if (info.isSymbolicLink()) {
|
|
916
|
+
throw unsafeMemoryPath(full, "symbolic links are not allowed in memory storage");
|
|
917
|
+
}
|
|
918
|
+
if (info.isDirectory()) {
|
|
919
|
+
assertSafeTree(root, full);
|
|
920
|
+
} else if (!info.isFile()) {
|
|
921
|
+
throw unsafeMemoryPath(full, "special files are not allowed in memory storage");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function walkMd(root, dir, prefix, out) {
|
|
926
|
+
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
927
|
+
const full = join12(dir, entry.name);
|
|
928
|
+
const info = lstatSync(full);
|
|
929
|
+
if (info.isSymbolicLink()) {
|
|
930
|
+
throw unsafeMemoryPath(full, "symbolic links are not allowed in memory storage");
|
|
931
|
+
}
|
|
932
|
+
if (entry.name.startsWith(".")) continue;
|
|
933
|
+
const key = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
934
|
+
if (info.isDirectory()) {
|
|
935
|
+
walkMd(root, full, key, out);
|
|
936
|
+
} else if (info.isFile() && entry.name.endsWith(".md")) {
|
|
937
|
+
const { content, stats } = readSafeFile(root, full);
|
|
938
|
+
out.push({ key, content, updatedAt: stats.mtimeMs });
|
|
939
|
+
} else if (!info.isFile()) {
|
|
940
|
+
throw unsafeMemoryPath(full, "special files are not allowed in memory storage");
|
|
699
941
|
}
|
|
700
942
|
}
|
|
701
943
|
}
|
|
702
944
|
function qmdBackend(root) {
|
|
945
|
+
let canonicalRoot;
|
|
946
|
+
const safeRoot = () => {
|
|
947
|
+
if (!canonicalRoot) canonicalRoot = initializeSafeRoot(root);
|
|
948
|
+
else assertRootUnchanged(root, canonicalRoot);
|
|
949
|
+
return canonicalRoot;
|
|
950
|
+
};
|
|
703
951
|
return {
|
|
704
952
|
async init() {
|
|
705
|
-
|
|
706
|
-
|
|
953
|
+
const safe = safeRoot();
|
|
954
|
+
assertSafeTree(safe);
|
|
955
|
+
const store = await getStore(safe);
|
|
707
956
|
await store.update();
|
|
708
957
|
},
|
|
709
958
|
async read(key) {
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}
|
|
714
|
-
const stats = statSync3(path);
|
|
959
|
+
const safe = safeRoot();
|
|
960
|
+
const path = safeKey(safe, key);
|
|
961
|
+
const { content, stats } = readSafeFile(safe, path);
|
|
715
962
|
return {
|
|
716
963
|
key,
|
|
717
|
-
content
|
|
964
|
+
content,
|
|
718
965
|
updatedAt: stats.mtimeMs
|
|
719
966
|
};
|
|
720
967
|
},
|
|
721
968
|
async write(key, content) {
|
|
722
|
-
const
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
const stats =
|
|
726
|
-
|
|
969
|
+
const safe = safeRoot();
|
|
970
|
+
assertSafeTree(safe);
|
|
971
|
+
const path = safeKey(safe, key);
|
|
972
|
+
const stats = writeSafeFile(safe, path, content);
|
|
973
|
+
assertSafeTree(safe);
|
|
974
|
+
const store = await getStore(safe);
|
|
727
975
|
await store.update();
|
|
728
976
|
return { key, content, updatedAt: stats.mtimeMs };
|
|
729
977
|
},
|
|
730
978
|
async search(query, opts) {
|
|
731
979
|
const limit = opts?.limit ?? 10;
|
|
732
|
-
const
|
|
980
|
+
const safe = safeRoot();
|
|
981
|
+
assertSafeTree(safe);
|
|
982
|
+
const store = await getStore(safe);
|
|
733
983
|
const results = await store.searchLex(query, {
|
|
734
984
|
limit,
|
|
735
985
|
collection: COLLECTION_NAME
|
|
@@ -738,13 +988,13 @@ function qmdBackend(root) {
|
|
|
738
988
|
for (const r of results) {
|
|
739
989
|
const prefix = `${COLLECTION_NAME}/`;
|
|
740
990
|
const key = r.displayPath.startsWith(prefix) ? r.displayPath.slice(prefix.length) : r.displayPath;
|
|
991
|
+
const path = safeKey(safe, key);
|
|
992
|
+
const info = inspectSafePath(safe, path);
|
|
993
|
+
if (!info) continue;
|
|
994
|
+
if (!info.isFile()) throw unsafeMemoryPath(path, "search result is not a regular file");
|
|
741
995
|
let content = r.body ?? "";
|
|
742
996
|
if (!content) {
|
|
743
|
-
|
|
744
|
-
content = readFileSync8(join12(root, key), "utf8");
|
|
745
|
-
} catch {
|
|
746
|
-
content = "";
|
|
747
|
-
}
|
|
997
|
+
content = readSafeFile(safe, path).content;
|
|
748
998
|
}
|
|
749
999
|
const snippet = extractSnippet(content, query).snippet;
|
|
750
1000
|
hits.push({ key, snippet, score: r.score });
|
|
@@ -752,14 +1002,23 @@ function qmdBackend(root) {
|
|
|
752
1002
|
return hits;
|
|
753
1003
|
},
|
|
754
1004
|
async list() {
|
|
1005
|
+
const safe = safeRoot();
|
|
1006
|
+
assertSafeTree(safe);
|
|
755
1007
|
const out = [];
|
|
756
|
-
walkMd(
|
|
1008
|
+
walkMd(safe, safe, "", out);
|
|
757
1009
|
return out.sort((a, b) => a.key.localeCompare(b.key));
|
|
758
1010
|
},
|
|
759
1011
|
async remove(key) {
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
const
|
|
1012
|
+
const safe = safeRoot();
|
|
1013
|
+
assertSafeTree(safe);
|
|
1014
|
+
const path = safeKey(safe, key);
|
|
1015
|
+
const info = inspectSafePath(safe, path);
|
|
1016
|
+
if (info) {
|
|
1017
|
+
if (!info.isFile()) throw unsafeMemoryPath(path, "memory entry is not a regular file");
|
|
1018
|
+
rmSync9(path);
|
|
1019
|
+
}
|
|
1020
|
+
assertSafeTree(safe);
|
|
1021
|
+
const store = await getStore(safe);
|
|
763
1022
|
await store.update();
|
|
764
1023
|
}
|
|
765
1024
|
};
|
|
@@ -915,8 +1174,8 @@ function piMessagesToProviderView(messages) {
|
|
|
915
1174
|
}
|
|
916
1175
|
|
|
917
1176
|
// ../daemon/src/runtime/pi/session.ts
|
|
918
|
-
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as
|
|
919
|
-
import { basename as basename3, join as
|
|
1177
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync5, readdirSync as readdirSync8, statSync as statSync5 } from "fs";
|
|
1178
|
+
import { basename as basename3, join as join18 } from "path";
|
|
920
1179
|
import {
|
|
921
1180
|
AuthStorage,
|
|
922
1181
|
createAgentSession,
|
|
@@ -976,15 +1235,38 @@ function resolveModel(cfg, modelId) {
|
|
|
976
1235
|
import { loginOpenAICodex, refreshOpenAICodexToken } from "@earendil-works/pi-ai/oauth";
|
|
977
1236
|
|
|
978
1237
|
// ../daemon/src/runtime/session/prompt.ts
|
|
979
|
-
import { existsSync as
|
|
1238
|
+
import { existsSync as existsSync10, readdirSync as readdirSync6, readFileSync as readFileSync9 } from "fs";
|
|
980
1239
|
import { join as join13 } from "path";
|
|
1240
|
+
var SANDBOX_SKILLS_DIR = "/skills";
|
|
1241
|
+
function loadPromptSkills(skillsDir, attachedNames) {
|
|
1242
|
+
if (!existsSync10(skillsDir) || attachedNames.length === 0) return [];
|
|
1243
|
+
const attached = new Set(attachedNames);
|
|
1244
|
+
const discovered = readdirSync6(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && attached.has(entry.name)).sort((a, b) => a.name.localeCompare(b.name));
|
|
1245
|
+
const loaded = [];
|
|
1246
|
+
for (const entry of discovered) {
|
|
1247
|
+
const hostDir = join13(skillsDir, entry.name);
|
|
1248
|
+
try {
|
|
1249
|
+
const parsed = parseSkillFile(join13(hostDir, "SKILL.md"));
|
|
1250
|
+
const safeLabel = entry.name.replace(/[^A-Za-z0-9._-]/g, "_") || "skill";
|
|
1251
|
+
loaded.push({
|
|
1252
|
+
name: entry.name,
|
|
1253
|
+
description: parsed.frontmatter.description,
|
|
1254
|
+
body: parsed.body.trim(),
|
|
1255
|
+
hostDir,
|
|
1256
|
+
sandboxDir: `${SANDBOX_SKILLS_DIR}/${loaded.length}-${safeLabel}`
|
|
1257
|
+
});
|
|
1258
|
+
} catch {
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return loaded;
|
|
1262
|
+
}
|
|
981
1263
|
var CONTEXT_FILE_ORDER = ["AGENTS.md", "SOUL.md", "TOOLS.md", "IDENTITY.md"];
|
|
982
|
-
function buildSystemPrompt(agent) {
|
|
1264
|
+
function buildSystemPrompt(agent, options = {}) {
|
|
983
1265
|
const parts = [];
|
|
984
1266
|
const contextBlocks = [];
|
|
985
1267
|
for (const file of CONTEXT_FILE_ORDER) {
|
|
986
1268
|
const path = join13(agent.agent.dir, file);
|
|
987
|
-
if (!
|
|
1269
|
+
if (!existsSync10(path)) continue;
|
|
988
1270
|
const content = readFileSync9(path, "utf8").trimEnd();
|
|
989
1271
|
if (!content) continue;
|
|
990
1272
|
contextBlocks.push(`## ${file}
|
|
@@ -997,7 +1279,7 @@ ${content}`);
|
|
|
997
1279
|
${contextBlocks.join("\n\n")}`);
|
|
998
1280
|
}
|
|
999
1281
|
const bootstrapPath = join13(agent.agent.dir, "BOOTSTRAP.md");
|
|
1000
|
-
if (
|
|
1282
|
+
if (existsSync10(bootstrapPath)) {
|
|
1001
1283
|
const bootstrap = readFileSync9(bootstrapPath, "utf8").trimEnd();
|
|
1002
1284
|
if (bootstrap) {
|
|
1003
1285
|
parts.push(
|
|
@@ -1027,14 +1309,35 @@ ${contextBlocks.join("\n\n")}`);
|
|
|
1027
1309
|
"- To change who you are (name, vibe, personality, how you behave): use `home_write`.",
|
|
1028
1310
|
"- To inspect exact wording of your own files: use `home_read` or `home_list`.",
|
|
1029
1311
|
"- To remember facts the user told you or things you learned: use `memory_write` \u2014 NOT `home_write`.",
|
|
1030
|
-
"- To produce work output (code, docs, artefacts): use
|
|
1312
|
+
"- To produce work output (code, docs, artefacts): use the workspace tools currently exposed to you \u2014 those operate on your team workspace, not your private home."
|
|
1031
1313
|
].join("\n")
|
|
1032
1314
|
);
|
|
1033
1315
|
if (agent.skills.length > 0) {
|
|
1316
|
+
const loaded = options.skills ?? [];
|
|
1317
|
+
const loadedNames = new Set(loaded.map((skill) => skill.name));
|
|
1318
|
+
const unavailable = agent.skills.filter((name) => !loadedNames.has(name));
|
|
1319
|
+
const skillBlocks = loaded.map((skill) => {
|
|
1320
|
+
const runtimeDir = options.sandboxMode === "docker" ? skill.sandboxDir : skill.hostDir;
|
|
1321
|
+
return [
|
|
1322
|
+
`## ${skill.name}`,
|
|
1323
|
+
"",
|
|
1324
|
+
skill.description,
|
|
1325
|
+
"",
|
|
1326
|
+
`Runtime directory: \`${runtimeDir}\`. Resolve any relative scripts or assets mentioned by this skill from that directory.`,
|
|
1327
|
+
"",
|
|
1328
|
+
skill.body
|
|
1329
|
+
].filter((line, index, lines) => line !== "" || lines[index - 1] !== "").join("\n").trimEnd();
|
|
1330
|
+
});
|
|
1331
|
+
const unavailableNote = unavailable.length > 0 ? `Attached but unavailable or invalid: ${unavailable.join(", ")}.` : "";
|
|
1034
1332
|
parts.push(
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1333
|
+
[
|
|
1334
|
+
"# Available Skills",
|
|
1335
|
+
"",
|
|
1336
|
+
"Use these attached skill instructions when they are relevant to the task.",
|
|
1337
|
+
"",
|
|
1338
|
+
...skillBlocks,
|
|
1339
|
+
...unavailableNote ? ["", unavailableNote] : []
|
|
1340
|
+
].join("\n")
|
|
1038
1341
|
);
|
|
1039
1342
|
}
|
|
1040
1343
|
const teamLines = [
|
|
@@ -1042,7 +1345,7 @@ You have access to the following skills: ${agent.skills.join(", ")}.`
|
|
|
1042
1345
|
"",
|
|
1043
1346
|
`- ${agent.team.id} (${agent.team.name}): ${agent.team.path}`,
|
|
1044
1347
|
"",
|
|
1045
|
-
"Your team is where work product lives \u2014 code, docs, artefacts, shared scratch. It may be shared with other agents in the same team.
|
|
1348
|
+
"Your team is where work product lives \u2014 code, docs, artefacts, shared scratch. It may be shared with other agents in the same team. Use the workspace tools currently exposed to work there. Never use workspace tools to edit your identity/soul/behaviour files \u2014 those live in your private home and are reached via `home_write` / `home_read`."
|
|
1046
1349
|
];
|
|
1047
1350
|
parts.push(teamLines.join("\n"));
|
|
1048
1351
|
if (agent.team.userMd.trim()) {
|
|
@@ -1060,18 +1363,1069 @@ ${agent.team.userMd.trim()}`
|
|
|
1060
1363
|
This team's USER.md is empty. As you learn STABLE facts about the human (preferences, role, working hours, how they like to be addressed), populate it via \`user_md_get\` then \`user_md_write\` (always get first \u2014 you need the etag). Reserve this for things you're confident are durable \u2014 project knowledge belongs in \`memory_write\`, personal notes about yourself in \`home_write\` on IDENTITY.md. **Do NOT send peer messages announcing USER.md changes \u2014 every agent in the team sees the new content in their system prompt on their next turn automatically.**`
|
|
1061
1364
|
);
|
|
1062
1365
|
}
|
|
1366
|
+
const reviewedLessons = boundReviewedLessons(agent.privateLessons);
|
|
1367
|
+
if (reviewedLessons.length > 0) {
|
|
1368
|
+
parts.push(
|
|
1369
|
+
`# Reviewed lessons
|
|
1370
|
+
|
|
1371
|
+
These private lessons were explicitly approved by the human for your behavior. Follow them when relevant; current user instructions and higher-priority policy still win.
|
|
1372
|
+
|
|
1373
|
+
${reviewedLessons.map((lesson) => `- ${lesson}`).join("\n")}`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1063
1376
|
parts.push(
|
|
1064
1377
|
"# Memory\n\nYou share a persistent memory backend with every other agent in this team. Use `memory_write` to remember things across sessions, and `memory_search` / `memory_read` / `memory_list` to recall them. This memory is for project knowledge \u2014 codebase notes, decisions, things the user told you about the work. For personal notes about yourself (preferences, persona quirks), use `home_write` on IDENTITY.md instead. Always check memory at the start of a session: another agent in the team may have already learned something useful. **Do NOT send peer messages announcing memory writes \u2014 every agent has access to the same store via `memory_search` and will find your note when they need it.**"
|
|
1065
1378
|
);
|
|
1066
1379
|
return parts.join("\n\n---\n\n");
|
|
1067
1380
|
}
|
|
1381
|
+
function boundReviewedLessons(lessons, maxCharacters = 8e3) {
|
|
1382
|
+
const selected = [];
|
|
1383
|
+
let used = 0;
|
|
1384
|
+
for (const raw of lessons) {
|
|
1385
|
+
const lesson = raw.trim();
|
|
1386
|
+
if (!lesson) continue;
|
|
1387
|
+
const remaining = maxCharacters - used;
|
|
1388
|
+
if (remaining <= 0) break;
|
|
1389
|
+
selected.push(lesson.slice(0, remaining));
|
|
1390
|
+
used += Math.min(lesson.length, remaining);
|
|
1391
|
+
}
|
|
1392
|
+
return selected;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// ../daemon/src/runtime/shell/tooling.ts
|
|
1396
|
+
import { existsSync as existsSync11, lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
|
|
1397
|
+
import { dirname as dirname3, isAbsolute as isAbsolute3, join as join15, relative as relative3, sep as sep4 } from "path";
|
|
1398
|
+
import {
|
|
1399
|
+
createBashToolDefinition,
|
|
1400
|
+
createLocalBashOperations,
|
|
1401
|
+
defineTool as defineTool2
|
|
1402
|
+
} from "@earendil-works/pi-coding-agent";
|
|
1403
|
+
|
|
1404
|
+
// ../daemon/src/runtime/shell/approval.ts
|
|
1405
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
1406
|
+
|
|
1407
|
+
// ../daemon/src/runtime/shell/security.ts
|
|
1408
|
+
var DEFAULT_BASH_SANDBOX_IMAGE = "debian:bookworm-slim";
|
|
1409
|
+
var SAFE_SHELL_ENV_KEYS = [
|
|
1410
|
+
"PATH",
|
|
1411
|
+
"HOME",
|
|
1412
|
+
"LANG",
|
|
1413
|
+
"LANGUAGE",
|
|
1414
|
+
"LC_ALL",
|
|
1415
|
+
"LC_CTYPE",
|
|
1416
|
+
"TERM",
|
|
1417
|
+
"SHELL",
|
|
1418
|
+
"TMPDIR",
|
|
1419
|
+
"TZ"
|
|
1420
|
+
];
|
|
1421
|
+
var ENV_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1422
|
+
var RESERVED_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1423
|
+
var DOCKER_IMAGE_RE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]*$/;
|
|
1424
|
+
var DOCKER_CLIENT_CONTROL_ENV = /* @__PURE__ */ new Set([
|
|
1425
|
+
"DOCKER_HOST",
|
|
1426
|
+
"DOCKER_CONTEXT",
|
|
1427
|
+
"DOCKER_CONFIG",
|
|
1428
|
+
"DOCKER_CERT_PATH",
|
|
1429
|
+
"DOCKER_TLS_VERIFY",
|
|
1430
|
+
"DOCKER_API_VERSION",
|
|
1431
|
+
"XDG_CONFIG_HOME",
|
|
1432
|
+
"XDG_RUNTIME_DIR"
|
|
1433
|
+
]);
|
|
1434
|
+
var SANDBOX_CONTROL_ENV = /* @__PURE__ */ new Set(["BASH_ENV"]);
|
|
1435
|
+
var SENSITIVE_HOME_PATH_RE = /(?:~(?:[A-Za-z0-9_-]+)?|\$(?:HOME|\{HOME\})|\/(?:home|Users)\/[^\s'"`;|&/]+|\/root)\/\.(?:ssh|aws|gnupg)(?:\/[^\s'"`;|&]*)?/gi;
|
|
1436
|
+
var SENSITIVE_FILE_RE = /(?:\bauth\.json\b|\bbazilion\.db\b|(?:^|[\s/'"=])\.env(?:\.[A-Za-z0-9_-]+)?(?=$|[\s/'"`;|&])|\/etc\/(?:shadow|sudoers)(?:\.d\/[^\s'"`;|&]+)?)/gim;
|
|
1437
|
+
var SENSITIVE_ENV_RE = /\b(?:OPENAI_CODEX_OAUTH|AWS_ACCESS_KEY_ID|AWS_PROFILE|GOOGLE_APPLICATION_CREDENTIALS|BAZILION_TOKEN|(?:[A-Z][A-Z0-9_]*_)?(?:API_KEY|ACCESS_TOKEN|AUTH_TOKEN|OAUTH_TOKEN|CLIENT_SECRET|SECRET_ACCESS_KEY|PRIVATE_KEY))\b/g;
|
|
1438
|
+
var REMOTE_PIPE_RE = /(?:^|[;&|(\n])\s*(?:command\s+)?(?:curl|wget)\b[^;&\n]*(?:\|\s*(?:(?:sudo|env)\s+)*(?:[^\s;&|()]+\/)?(?:sh|bash|dash|zsh|fish|node|python(?:3)?|perl|ruby)\b)/gim;
|
|
1439
|
+
var COMMAND_BOUNDARY_RE = /[;&|)\n]/;
|
|
1440
|
+
var ShellSecurityConfigError = class extends Error {
|
|
1441
|
+
constructor(message) {
|
|
1442
|
+
super(message);
|
|
1443
|
+
this.name = "ShellSecurityConfigError";
|
|
1444
|
+
}
|
|
1445
|
+
};
|
|
1446
|
+
function isValidEnvName(name) {
|
|
1447
|
+
return ENV_NAME_RE.test(name) && !RESERVED_OBJECT_KEYS.has(name);
|
|
1448
|
+
}
|
|
1449
|
+
function addRisk(command, risks, seen, input) {
|
|
1450
|
+
const start = Math.max(0, input.start);
|
|
1451
|
+
const end = Math.min(command.length, Math.max(start, input.end));
|
|
1452
|
+
const key = `${input.code}:${start}:${end}`;
|
|
1453
|
+
if (seen.has(key)) return;
|
|
1454
|
+
seen.add(key);
|
|
1455
|
+
risks.push({
|
|
1456
|
+
code: input.code,
|
|
1457
|
+
severity: input.severity ?? "danger",
|
|
1458
|
+
message: input.message,
|
|
1459
|
+
matchedText: command.slice(start, end),
|
|
1460
|
+
span: { start, end }
|
|
1461
|
+
});
|
|
1462
|
+
}
|
|
1463
|
+
function addRegexRisks(command, risks, seen, pattern, code, message) {
|
|
1464
|
+
pattern.lastIndex = 0;
|
|
1465
|
+
for (const match of command.matchAll(pattern)) {
|
|
1466
|
+
if (match.index === void 0) continue;
|
|
1467
|
+
let start = match.index;
|
|
1468
|
+
let matchedText = match[0];
|
|
1469
|
+
const leading = matchedText.match(/^[;&|(\n\s]+/);
|
|
1470
|
+
if (leading) {
|
|
1471
|
+
start += leading[0].length;
|
|
1472
|
+
matchedText = matchedText.slice(leading[0].length);
|
|
1473
|
+
}
|
|
1474
|
+
addRisk(command, risks, seen, {
|
|
1475
|
+
code,
|
|
1476
|
+
message,
|
|
1477
|
+
start,
|
|
1478
|
+
end: start + matchedText.length
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function shellTokens(fragment) {
|
|
1483
|
+
return Array.from(
|
|
1484
|
+
fragment.matchAll(/"(?:\\.|[^"\\])*"|'[^']*'|[^\s]+/g),
|
|
1485
|
+
(match) => match[0].replace(/^(?:"|')|(?:"|')$/g, "")
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
function isBroadFilesystemTarget(rawTarget) {
|
|
1489
|
+
const target = rawTarget.replace(/[;,]+$/, "");
|
|
1490
|
+
if (target.startsWith("/")) return true;
|
|
1491
|
+
return /^(?:\.|\.\/\*+|\*+|~(?:\/.*)?|\$(?:HOME|\{HOME\})(?:\/.*)?|\.\.(?:\/.*)?)$/.test(target);
|
|
1492
|
+
}
|
|
1493
|
+
function classifyBroadFilesystemOperations(command, risks, seen) {
|
|
1494
|
+
const commandRe = /(?:^|[;&|(\n])\s*(?:command\s+)?(rm|find|chmod|chown)\b[^;&|)\n]*/gim;
|
|
1495
|
+
for (const match of command.matchAll(commandRe)) {
|
|
1496
|
+
if (match.index === void 0 || !match[1]) continue;
|
|
1497
|
+
const nameOffset = match[0].toLowerCase().indexOf(match[1].toLowerCase());
|
|
1498
|
+
const start = match.index + nameOffset;
|
|
1499
|
+
const text = command.slice(start, match.index + match[0].length).trimEnd();
|
|
1500
|
+
const tokens = shellTokens(text);
|
|
1501
|
+
const name = tokens[0]?.toLowerCase();
|
|
1502
|
+
const args = tokens.slice(1);
|
|
1503
|
+
let risky = false;
|
|
1504
|
+
if (name === "rm") {
|
|
1505
|
+
const recursive = args.some(
|
|
1506
|
+
(arg) => arg === "--recursive" || /^-[^-]+$/.test(arg) && /[rR]/.test(arg)
|
|
1507
|
+
);
|
|
1508
|
+
risky = recursive && args.some((arg) => !arg.startsWith("-") && isBroadFilesystemTarget(arg));
|
|
1509
|
+
} else if (name === "find") {
|
|
1510
|
+
const target = args.find((arg) => !arg.startsWith("-"));
|
|
1511
|
+
risky = args.includes("-delete") && target !== void 0 && isBroadFilesystemTarget(target);
|
|
1512
|
+
} else if (name === "chmod" || name === "chown") {
|
|
1513
|
+
const recursive = args.some(
|
|
1514
|
+
(arg) => arg === "--recursive" || /^-[^-]+$/.test(arg) && /R/.test(arg)
|
|
1515
|
+
);
|
|
1516
|
+
risky = recursive && args.some((arg) => !arg.startsWith("-") && isBroadFilesystemTarget(arg));
|
|
1517
|
+
}
|
|
1518
|
+
if (!risky) continue;
|
|
1519
|
+
addRisk(command, risks, seen, {
|
|
1520
|
+
code: "broad-destructive-operation",
|
|
1521
|
+
message: "Recursively changes or deletes a broad filesystem target.",
|
|
1522
|
+
start,
|
|
1523
|
+
end: start + text.length
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
function isLocalHostname(rawHostname) {
|
|
1528
|
+
const hostname = rawHostname.trim().replace(/^.*@/, "").replace(/^\[|\]$/g, "").replace(/\.$/, "").toLowerCase();
|
|
1529
|
+
return hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || hostname === "0.0.0.0" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
|
|
1530
|
+
}
|
|
1531
|
+
function hasExternalHttpDestination(segment) {
|
|
1532
|
+
const urls = Array.from(segment.matchAll(/https?:\/\/[^\s'"`]+/gi), (match) => match[0]);
|
|
1533
|
+
if (urls.length === 0) return true;
|
|
1534
|
+
return urls.some((rawUrl) => {
|
|
1535
|
+
try {
|
|
1536
|
+
return !isLocalHostname(new URL(rawUrl).hostname);
|
|
1537
|
+
} catch {
|
|
1538
|
+
return true;
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
function remoteTransferHosts(segment) {
|
|
1543
|
+
const hosts = [];
|
|
1544
|
+
const uriRe = /(?:sftp|ssh|rsync):\/\/(?:[^@\s/]+@)?(\[[^\]]+\]|[^\s/:]+)(?::\d+)?/gi;
|
|
1545
|
+
for (const match of segment.matchAll(uriRe)) {
|
|
1546
|
+
if (match[1]) hosts.push(match[1]);
|
|
1547
|
+
}
|
|
1548
|
+
const scpRe = /(?:^|\s)(?:[^@\s:]+@)?(\[[^\]]+\]|[A-Za-z0-9._-]+):[^\s]+/g;
|
|
1549
|
+
for (const match of segment.matchAll(scpRe)) {
|
|
1550
|
+
if (match[1]) hosts.push(match[1]);
|
|
1551
|
+
}
|
|
1552
|
+
return hosts;
|
|
1553
|
+
}
|
|
1554
|
+
function invocationSegment(command, match, commandName) {
|
|
1555
|
+
if (match.index === void 0) return null;
|
|
1556
|
+
const nameOffset = match[0].toLowerCase().indexOf(commandName.toLowerCase());
|
|
1557
|
+
if (nameOffset < 0) return null;
|
|
1558
|
+
const start = match.index + nameOffset;
|
|
1559
|
+
const boundaryOffset = command.slice(start).search(COMMAND_BOUNDARY_RE);
|
|
1560
|
+
const end = boundaryOffset < 0 ? command.length : start + boundaryOffset;
|
|
1561
|
+
return { start, text: command.slice(start, end).trimEnd() };
|
|
1562
|
+
}
|
|
1563
|
+
function classifyOutboundExfiltration(command, risks, seen) {
|
|
1564
|
+
const invocationRe = /(?:^|[;&|(\n])\s*(?:command\s+)?(curl|wget|scp|rsync|nc|netcat|sftp)\b/gim;
|
|
1565
|
+
for (const match of command.matchAll(invocationRe)) {
|
|
1566
|
+
const name = match[1]?.toLowerCase();
|
|
1567
|
+
if (!name) continue;
|
|
1568
|
+
const segment = invocationSegment(command, match, name);
|
|
1569
|
+
if (!segment) continue;
|
|
1570
|
+
let risky = false;
|
|
1571
|
+
if (name === "curl") {
|
|
1572
|
+
const uploads = /(?:^|\s)(?:-X\s*POST\b|-[dFT](?:\s|[^\s]))/.test(segment.text) || /(?:^|\s)(?:--request(?:=|\s+)POST\b|--data(?:-[a-z-]+)?(?:=|\s)|--form(?:-string)?(?:=|\s)|--upload-file(?:=|\s))/i.test(
|
|
1573
|
+
segment.text
|
|
1574
|
+
);
|
|
1575
|
+
risky = uploads && hasExternalHttpDestination(segment.text);
|
|
1576
|
+
} else if (name === "wget") {
|
|
1577
|
+
const uploads = /(?:^|\s)(?:--method(?:=|\s+)POST\b|--post-data(?:=|\s)|--post-file(?:=|\s)|--body-data(?:=|\s)|--body-file(?:=|\s))/i.test(
|
|
1578
|
+
segment.text
|
|
1579
|
+
);
|
|
1580
|
+
risky = uploads && hasExternalHttpDestination(segment.text);
|
|
1581
|
+
} else if (name === "scp" || name === "rsync") {
|
|
1582
|
+
const hosts = remoteTransferHosts(segment.text);
|
|
1583
|
+
risky = hosts.some((host) => !isLocalHostname(host)) || hosts.length === 0 && /\$/.test(segment.text);
|
|
1584
|
+
} else if (name === "sftp") {
|
|
1585
|
+
const hosts = remoteTransferHosts(segment.text);
|
|
1586
|
+
if (hosts.length > 0) {
|
|
1587
|
+
risky = hosts.some((host) => !isLocalHostname(host));
|
|
1588
|
+
} else {
|
|
1589
|
+
const destination = shellTokens(segment.text).at(-1);
|
|
1590
|
+
risky = destination !== void 0 && destination !== name && !isLocalHostname(destination);
|
|
1591
|
+
}
|
|
1592
|
+
} else {
|
|
1593
|
+
const tokens = shellTokens(segment.text);
|
|
1594
|
+
const host = tokens.find(
|
|
1595
|
+
(token, index) => index > 0 && !token.startsWith("-") && !/^\d+$/.test(token) && !["tcp", "udp"].includes(token.toLowerCase())
|
|
1596
|
+
);
|
|
1597
|
+
risky = host !== void 0 && !isLocalHostname(host);
|
|
1598
|
+
}
|
|
1599
|
+
if (!risky) continue;
|
|
1600
|
+
addRisk(command, risks, seen, {
|
|
1601
|
+
code: "outbound-exfiltration",
|
|
1602
|
+
message: "Uploads data or opens a transfer connection to a non-local destination.",
|
|
1603
|
+
start: segment.start,
|
|
1604
|
+
end: segment.start + segment.text.length
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function classifyPrivilegeAndCredentialAccess(command, risks, seen) {
|
|
1609
|
+
const patterns = [
|
|
1610
|
+
/(?:^|[;&|(\n])\s*(?:sudo|su|doas|pkexec)\b[^;&|)\n]*/gim,
|
|
1611
|
+
/(?:^|[;&|(\n])\s*security\s+(?:find-(?:generic|internet)-password|dump-keychain)\b[^;&|)\n]*/gim,
|
|
1612
|
+
/(?:^|[;&|(\n])\s*gcloud\s+auth\b[^;&|)\n]*/gim,
|
|
1613
|
+
/(?:^|[;&|(\n])\s*aws\s+(?:configure|sso\s+login)\b[^;&|)\n]*/gim,
|
|
1614
|
+
/(?:^|[;&|(\n])\s*git\s+credential(?:-[A-Za-z0-9_-]+|\s+(?:fill|approve|reject))?\b[^;&|)\n]*/gim,
|
|
1615
|
+
/(?:^|[;&|(\n])\s*(?:docker|npm|gh)\s+(?:login|auth\s+token)\b[^;&|)\n]*/gim,
|
|
1616
|
+
/(?:^|[;&|(\n])\s*(?:pass\s+(?:show|ls)|secret-tool\s+lookup|ssh-add\s+-[Ll])\b[^;&|)\n]*/gim,
|
|
1617
|
+
/(?:^|[;&|(\n])\s*(?:env|printenv|set|export\s+-p)\s*(?=$|[;&|)\n])/gim
|
|
1618
|
+
];
|
|
1619
|
+
for (const pattern of patterns) {
|
|
1620
|
+
addRegexRisks(
|
|
1621
|
+
command,
|
|
1622
|
+
risks,
|
|
1623
|
+
seen,
|
|
1624
|
+
pattern,
|
|
1625
|
+
"privilege-or-credential-access",
|
|
1626
|
+
"Requests elevated privileges or accesses a credential store."
|
|
1627
|
+
);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
function classifyBashCommand(command) {
|
|
1631
|
+
const risks = [];
|
|
1632
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1633
|
+
addRegexRisks(
|
|
1634
|
+
command,
|
|
1635
|
+
risks,
|
|
1636
|
+
seen,
|
|
1637
|
+
SENSITIVE_HOME_PATH_RE,
|
|
1638
|
+
"sensitive-path-read",
|
|
1639
|
+
"Accesses a path that commonly contains credentials."
|
|
1640
|
+
);
|
|
1641
|
+
addRegexRisks(
|
|
1642
|
+
command,
|
|
1643
|
+
risks,
|
|
1644
|
+
seen,
|
|
1645
|
+
SENSITIVE_FILE_RE,
|
|
1646
|
+
"sensitive-path-read",
|
|
1647
|
+
"Accesses a sensitive environment, authentication, or database file."
|
|
1648
|
+
);
|
|
1649
|
+
addRegexRisks(
|
|
1650
|
+
command,
|
|
1651
|
+
risks,
|
|
1652
|
+
seen,
|
|
1653
|
+
SENSITIVE_ENV_RE,
|
|
1654
|
+
"sensitive-path-read",
|
|
1655
|
+
"References an environment variable name commonly used for credentials."
|
|
1656
|
+
);
|
|
1657
|
+
classifyBroadFilesystemOperations(command, risks, seen);
|
|
1658
|
+
addRegexRisks(
|
|
1659
|
+
command,
|
|
1660
|
+
risks,
|
|
1661
|
+
seen,
|
|
1662
|
+
REMOTE_PIPE_RE,
|
|
1663
|
+
"remote-pipe-execution",
|
|
1664
|
+
"Pipes downloaded content directly into an interpreter."
|
|
1665
|
+
);
|
|
1666
|
+
classifyOutboundExfiltration(command, risks, seen);
|
|
1667
|
+
classifyPrivilegeAndCredentialAccess(command, risks, seen);
|
|
1668
|
+
return risks.sort((a, b) => a.span.start - b.span.start || a.code.localeCompare(b.code));
|
|
1669
|
+
}
|
|
1670
|
+
function parseEnvAllowlist(raw) {
|
|
1671
|
+
if (raw === void 0 || raw.trim() === "") return [];
|
|
1672
|
+
const values = raw.split(",").map((value) => value.trim());
|
|
1673
|
+
if (values.some((value) => value === "")) {
|
|
1674
|
+
throw new ShellSecurityConfigError(
|
|
1675
|
+
"BAZILION_BASH_SANDBOX_ENV_ALLOWLIST contains an empty environment variable name"
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
const unique = [];
|
|
1679
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1680
|
+
for (const value of values) {
|
|
1681
|
+
if (!isValidEnvName(value)) {
|
|
1682
|
+
throw new ShellSecurityConfigError(
|
|
1683
|
+
`BAZILION_BASH_SANDBOX_ENV_ALLOWLIST contains invalid environment variable name: ${value}`
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
if (DOCKER_CLIENT_CONTROL_ENV.has(value)) {
|
|
1687
|
+
throw new ShellSecurityConfigError(
|
|
1688
|
+
`BAZILION_BASH_SANDBOX_ENV_ALLOWLIST cannot include Docker client control variable: ${value}`
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
if (SANDBOX_CONTROL_ENV.has(value)) {
|
|
1692
|
+
throw new ShellSecurityConfigError(
|
|
1693
|
+
`BAZILION_BASH_SANDBOX_ENV_ALLOWLIST cannot include shell startup control variable: ${value}`
|
|
1694
|
+
);
|
|
1695
|
+
}
|
|
1696
|
+
if (!seen.has(value)) {
|
|
1697
|
+
seen.add(value);
|
|
1698
|
+
unique.push(value);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
return unique;
|
|
1702
|
+
}
|
|
1703
|
+
function resolveShellSecurityConfig(env = process.env) {
|
|
1704
|
+
const rawMode = env.BAZILION_BASH_SANDBOX?.trim() ?? "";
|
|
1705
|
+
if (rawMode !== "" && rawMode !== "off" && rawMode !== "docker") {
|
|
1706
|
+
throw new ShellSecurityConfigError(
|
|
1707
|
+
`BAZILION_BASH_SANDBOX must be "off" or "docker", received: ${rawMode}`
|
|
1708
|
+
);
|
|
1709
|
+
}
|
|
1710
|
+
const rawApproval = env.BAZILION_BASH_APPROVAL?.trim() ?? "";
|
|
1711
|
+
if (rawApproval !== "" && rawApproval !== "off" && rawApproval !== "dangerous") {
|
|
1712
|
+
throw new ShellSecurityConfigError(
|
|
1713
|
+
`BAZILION_BASH_APPROVAL must be "off" or "dangerous", received: ${rawApproval}`
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
const rawImage = env.BAZILION_BASH_SANDBOX_IMAGE?.trim() ?? "";
|
|
1717
|
+
if (rawImage !== "" && !DOCKER_IMAGE_RE.test(rawImage)) {
|
|
1718
|
+
throw new ShellSecurityConfigError(
|
|
1719
|
+
`BAZILION_BASH_SANDBOX_IMAGE is not a valid Docker image reference: ${rawImage}`
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
return {
|
|
1723
|
+
sandboxMode: rawMode === "docker" ? "docker" : "off",
|
|
1724
|
+
approvalMode: rawApproval === "dangerous" ? "dangerous" : "off",
|
|
1725
|
+
sandboxImage: rawImage || DEFAULT_BASH_SANDBOX_IMAGE,
|
|
1726
|
+
envAllowlist: parseEnvAllowlist(env.BAZILION_BASH_SANDBOX_ENV_ALLOWLIST)
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
function assertEnvName(name) {
|
|
1730
|
+
if (!isValidEnvName(name)) {
|
|
1731
|
+
throw new ShellSecurityConfigError(`invalid shell environment variable name: ${name}`);
|
|
1732
|
+
}
|
|
1733
|
+
if (DOCKER_CLIENT_CONTROL_ENV.has(name) || SANDBOX_CONTROL_ENV.has(name)) {
|
|
1734
|
+
throw new ShellSecurityConfigError(`unsafe shell environment variable name: ${name}`);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
function buildScrubbedShellEnv(env, allowedKeys = []) {
|
|
1738
|
+
const scrubbed = {};
|
|
1739
|
+
const keys = new Set(SAFE_SHELL_ENV_KEYS);
|
|
1740
|
+
for (const key of allowedKeys) {
|
|
1741
|
+
assertEnvName(key);
|
|
1742
|
+
keys.add(key);
|
|
1743
|
+
}
|
|
1744
|
+
for (const key of keys) {
|
|
1745
|
+
const value = env[key];
|
|
1746
|
+
if (value !== void 0) scrubbed[key] = value;
|
|
1747
|
+
}
|
|
1748
|
+
return scrubbed;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// ../daemon/src/runtime/shell/approval.ts
|
|
1752
|
+
var BashApprovalDeniedError = class extends Error {
|
|
1753
|
+
risks;
|
|
1754
|
+
constructor(risks) {
|
|
1755
|
+
const codes = [...new Set(risks.map((risk) => risk.code))];
|
|
1756
|
+
super(`Dangerous bash command denied (${codes.join(", ")})`);
|
|
1757
|
+
this.name = "BashApprovalDeniedError";
|
|
1758
|
+
this.risks = risks;
|
|
1759
|
+
}
|
|
1760
|
+
};
|
|
1761
|
+
function createApprovalGatedBashTool(base, host) {
|
|
1762
|
+
return defineTool({
|
|
1763
|
+
...base,
|
|
1764
|
+
description: `${base.description} Commands classified as dangerous require operator approval before execution.`,
|
|
1765
|
+
executionMode: "sequential",
|
|
1766
|
+
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1767
|
+
const risks = classifyBashCommand(params.command);
|
|
1768
|
+
if (risks.length > 0) {
|
|
1769
|
+
const decision = host ? await host.requestApproval({
|
|
1770
|
+
toolCallId,
|
|
1771
|
+
command: params.command,
|
|
1772
|
+
risks,
|
|
1773
|
+
signal
|
|
1774
|
+
}) : "denied";
|
|
1775
|
+
if (decision !== "approved") throw new BashApprovalDeniedError(risks);
|
|
1776
|
+
}
|
|
1777
|
+
return base.execute(toolCallId, params, signal, onUpdate, ctx);
|
|
1778
|
+
}
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
// ../daemon/src/runtime/shell/docker.ts
|
|
1783
|
+
import { spawn } from "child_process";
|
|
1784
|
+
import { randomUUID as randomUUID17 } from "crypto";
|
|
1785
|
+
import { closeSync as closeSync2, constants as constants2, mkdtempSync as mkdtempSync2, openSync as openSync2, rmSync as rmSync10, writeFileSync as writeFileSync7 } from "fs";
|
|
1786
|
+
import { access, realpath, stat } from "fs/promises";
|
|
1787
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1788
|
+
import { isAbsolute as isAbsolute2, join as join14, relative as relative2, sep as sep3 } from "path";
|
|
1789
|
+
var CONTAINER_WORKDIR = "/workspace";
|
|
1790
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
1791
|
+
var MAX_STDERR_CHARS = 16 * 1024;
|
|
1792
|
+
var CLEANUP_TIMEOUT_MS = 650;
|
|
1793
|
+
var CLEANUP_RETRY_DELAY_MS = 100;
|
|
1794
|
+
var CLEANUP_ATTEMPTS = 3;
|
|
1795
|
+
var DOCKER_CONTEXT_TIMEOUT_MS = 2e3;
|
|
1796
|
+
var ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1797
|
+
var CONTAINER_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]+$/;
|
|
1798
|
+
var DOCKER_CLIENT_ENV_KEYS = [
|
|
1799
|
+
"PATH",
|
|
1800
|
+
"HOME",
|
|
1801
|
+
"DOCKER_HOST",
|
|
1802
|
+
"DOCKER_CONTEXT",
|
|
1803
|
+
"DOCKER_CONFIG",
|
|
1804
|
+
"DOCKER_CERT_PATH",
|
|
1805
|
+
"DOCKER_TLS_VERIFY",
|
|
1806
|
+
"DOCKER_API_VERSION",
|
|
1807
|
+
"XDG_CONFIG_HOME",
|
|
1808
|
+
"XDG_RUNTIME_DIR"
|
|
1809
|
+
];
|
|
1810
|
+
var DOCKER_CLIENT_CONTROL_ENV2 = new Set(DOCKER_CLIENT_ENV_KEYS);
|
|
1811
|
+
var SANDBOX_CONTROL_ENV2 = /* @__PURE__ */ new Set(["BASH_ENV"]);
|
|
1812
|
+
var CONTAINER_LITERAL_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
1813
|
+
"PATH",
|
|
1814
|
+
"HOME",
|
|
1815
|
+
"LANG",
|
|
1816
|
+
"LANGUAGE",
|
|
1817
|
+
"LC_ALL",
|
|
1818
|
+
"LC_CTYPE",
|
|
1819
|
+
"TERM",
|
|
1820
|
+
"SHELL",
|
|
1821
|
+
"TMPDIR",
|
|
1822
|
+
"TZ"
|
|
1823
|
+
]);
|
|
1824
|
+
function buildDockerRunSpec(input) {
|
|
1825
|
+
const executable = input.dockerPath ?? "docker";
|
|
1826
|
+
validateDockerPath(executable);
|
|
1827
|
+
validateImage(input.image);
|
|
1828
|
+
if (input.resolvedImage !== void 0) validateImage(input.resolvedImage);
|
|
1829
|
+
validateContainerName(input.containerName);
|
|
1830
|
+
validateMountArgument(input.mountSource);
|
|
1831
|
+
const readOnlyMounts = input.readOnlyMounts ?? [];
|
|
1832
|
+
validateReadOnlyMounts(readOnlyMounts, input.mountSource);
|
|
1833
|
+
validateHostIdentity(input.uid, input.gid);
|
|
1834
|
+
if (input.command.includes("\0")) {
|
|
1835
|
+
throw new Error("Docker sandbox command cannot contain a NUL byte");
|
|
1836
|
+
}
|
|
1837
|
+
const args = [
|
|
1838
|
+
"run",
|
|
1839
|
+
"--rm",
|
|
1840
|
+
"--pull",
|
|
1841
|
+
"never",
|
|
1842
|
+
"--name",
|
|
1843
|
+
input.containerName,
|
|
1844
|
+
"--network",
|
|
1845
|
+
"none",
|
|
1846
|
+
"--read-only",
|
|
1847
|
+
"--tmpfs",
|
|
1848
|
+
"/tmp:rw,nosuid,nodev,size=64m,mode=1777",
|
|
1849
|
+
"--user",
|
|
1850
|
+
`${input.uid}:${input.gid}`,
|
|
1851
|
+
"--cap-drop",
|
|
1852
|
+
"ALL",
|
|
1853
|
+
"--security-opt",
|
|
1854
|
+
"no-new-privileges=true",
|
|
1855
|
+
"--pids-limit",
|
|
1856
|
+
"256",
|
|
1857
|
+
"--entrypoint",
|
|
1858
|
+
"/bin/bash",
|
|
1859
|
+
"--workdir",
|
|
1860
|
+
CONTAINER_WORKDIR,
|
|
1861
|
+
// A bind mount is read/write unless `readonly` is present. No other mount is added.
|
|
1862
|
+
"--mount",
|
|
1863
|
+
`type=bind,source=${input.mountSource},target=${CONTAINER_WORKDIR},bind-recursive=disabled`
|
|
1864
|
+
];
|
|
1865
|
+
for (const mount of readOnlyMounts) {
|
|
1866
|
+
args.push(
|
|
1867
|
+
"--mount",
|
|
1868
|
+
`type=bind,source=${mount.source},target=${mount.target},readonly,bind-recursive=disabled`
|
|
1869
|
+
);
|
|
1870
|
+
}
|
|
1871
|
+
const processEnv = buildDockerClientEnv(input.hostEnv ?? process.env);
|
|
1872
|
+
const envFileLines = [];
|
|
1873
|
+
const containerEnvNames = [];
|
|
1874
|
+
for (const [name, value] of sortedEnvironment(input.env)) {
|
|
1875
|
+
containerEnvNames.push(name);
|
|
1876
|
+
if (CONTAINER_LITERAL_ENV_KEYS.has(name)) {
|
|
1877
|
+
args.push("--env", `${name}=${value}`);
|
|
1878
|
+
} else {
|
|
1879
|
+
envFileLines.push(`${name}=${value}`);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
if (envFileLines.length > 0) args.push("--env-file", "/dev/fd/3");
|
|
1883
|
+
args.push("--env", "BASH_ENV=");
|
|
1884
|
+
const cleanAssignments = containerEnvNames.map((name) => `${name}="$${name}"`).join(" ");
|
|
1885
|
+
const cleanEnvironmentCommand = `exec /usr/bin/env -i${cleanAssignments ? ` ${cleanAssignments}` : ""} /bin/bash --noprofile --norc -c "$1"`;
|
|
1886
|
+
args.push(
|
|
1887
|
+
input.resolvedImage ?? input.image,
|
|
1888
|
+
"--noprofile",
|
|
1889
|
+
"--norc",
|
|
1890
|
+
"-c",
|
|
1891
|
+
cleanEnvironmentCommand,
|
|
1892
|
+
"bazilion-sandbox",
|
|
1893
|
+
input.command
|
|
1894
|
+
);
|
|
1895
|
+
return {
|
|
1896
|
+
executable,
|
|
1897
|
+
args,
|
|
1898
|
+
containerName: input.containerName,
|
|
1899
|
+
image: input.image,
|
|
1900
|
+
mountSource: input.mountSource,
|
|
1901
|
+
processEnv,
|
|
1902
|
+
...envFileLines.length > 0 ? { envFileContent: `${envFileLines.join("\n")}
|
|
1903
|
+
` } : {}
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
function createDockerBashOperations(options) {
|
|
1907
|
+
const image = options.image;
|
|
1908
|
+
const env = { ...options.env };
|
|
1909
|
+
const dockerPath = options.dockerPath ?? "docker";
|
|
1910
|
+
const configuredReadOnlyMounts = options.readOnlyMounts ?? [];
|
|
1911
|
+
const hostEnv = { ...options.hostEnv ?? process.env };
|
|
1912
|
+
validateDockerPath(dockerPath);
|
|
1913
|
+
validateImage(image);
|
|
1914
|
+
return {
|
|
1915
|
+
async exec(command, cwd, { onData, signal, timeout }) {
|
|
1916
|
+
const timeoutMs = resolveTimeoutMs(timeout);
|
|
1917
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
1918
|
+
const mountSource = await resolveMountSource(cwd);
|
|
1919
|
+
const readOnlyMounts = await Promise.all(
|
|
1920
|
+
configuredReadOnlyMounts.map(async (mount) => {
|
|
1921
|
+
const source = await resolveMountSource(mount.source);
|
|
1922
|
+
if (mount.sourceRoot && !isWithin(mount.sourceRoot, source)) {
|
|
1923
|
+
throw new Error(
|
|
1924
|
+
`Docker sandbox read-only mount escaped its configured root: ${mount.source}`
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
return { source, target: mount.target };
|
|
1928
|
+
})
|
|
1929
|
+
);
|
|
1930
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
1931
|
+
const { uid, gid } = hostIdentity();
|
|
1932
|
+
const dockerClientEnv = await resolveLocalDockerClientEnv(dockerPath, hostEnv);
|
|
1933
|
+
const resolvedImage = await inspectSandboxImage(dockerPath, image, dockerClientEnv);
|
|
1934
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
1935
|
+
const containerName = `bazilion-bash-${process.pid}-${randomUUID17().replaceAll("-", "")}`;
|
|
1936
|
+
const spec = buildDockerRunSpec({
|
|
1937
|
+
image,
|
|
1938
|
+
env,
|
|
1939
|
+
dockerPath,
|
|
1940
|
+
command,
|
|
1941
|
+
mountSource,
|
|
1942
|
+
containerName,
|
|
1943
|
+
uid,
|
|
1944
|
+
gid,
|
|
1945
|
+
resolvedImage,
|
|
1946
|
+
readOnlyMounts,
|
|
1947
|
+
hostEnv: dockerClientEnv
|
|
1948
|
+
});
|
|
1949
|
+
return executeDockerRun(spec, { onData, signal, timeout, timeoutMs });
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
function executeDockerRun(spec, options) {
|
|
1954
|
+
let envFileFd;
|
|
1955
|
+
try {
|
|
1956
|
+
if (spec.envFileContent !== void 0) {
|
|
1957
|
+
envFileFd = openAnonymousEnvFile(spec.envFileContent);
|
|
1958
|
+
}
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
return Promise.reject(
|
|
1961
|
+
new Error(
|
|
1962
|
+
`Docker sandbox could not prepare its environment: ${error instanceof Error ? error.message : String(error)}`
|
|
1963
|
+
)
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
return new Promise((resolve5, reject) => {
|
|
1967
|
+
const child = spawn(spec.executable, spec.args, {
|
|
1968
|
+
stdio: ["ignore", "pipe", "pipe", envFileFd ?? "ignore"],
|
|
1969
|
+
windowsHide: true,
|
|
1970
|
+
env: spec.processEnv
|
|
1971
|
+
});
|
|
1972
|
+
if (envFileFd !== void 0) closeSync2(envFileFd);
|
|
1973
|
+
let settled = false;
|
|
1974
|
+
let termination;
|
|
1975
|
+
let timeoutHandle;
|
|
1976
|
+
let stderr = "";
|
|
1977
|
+
const requestTermination = (reason) => {
|
|
1978
|
+
termination ??= reason;
|
|
1979
|
+
child.kill("SIGKILL");
|
|
1980
|
+
};
|
|
1981
|
+
const onAbort = () => requestTermination("aborted");
|
|
1982
|
+
const finish = async (exitCode, spawnError) => {
|
|
1983
|
+
if (settled) return;
|
|
1984
|
+
settled = true;
|
|
1985
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1986
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
1987
|
+
if (!termination && options.signal?.aborted) termination = "aborted";
|
|
1988
|
+
if (termination) {
|
|
1989
|
+
await removeContainer(spec.executable, spec.containerName, spec.processEnv);
|
|
1990
|
+
reject(
|
|
1991
|
+
termination === "aborted" ? new Error("aborted") : new Error(`timeout:${options.timeout}`)
|
|
1992
|
+
);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
if (spawnError) {
|
|
1996
|
+
reject(formatSpawnError(spec.executable, spawnError));
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
if (exitCode === 125) {
|
|
2000
|
+
await removeContainer(spec.executable, spec.containerName, spec.processEnv);
|
|
2001
|
+
reject(formatDockerRunError(spec, stderr));
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
if (exitCode === null) {
|
|
2005
|
+
await removeContainer(spec.executable, spec.containerName, spec.processEnv);
|
|
2006
|
+
reject(new Error(`Docker sandbox CLI for container "${spec.containerName}" was killed`));
|
|
2007
|
+
return;
|
|
2008
|
+
}
|
|
2009
|
+
resolve5({ exitCode });
|
|
2010
|
+
};
|
|
2011
|
+
child.stdout?.on("data", options.onData);
|
|
2012
|
+
child.stderr?.on("data", (data) => {
|
|
2013
|
+
stderr = `${stderr}${data.toString("utf8")}`.slice(-MAX_STDERR_CHARS);
|
|
2014
|
+
options.onData(data);
|
|
2015
|
+
});
|
|
2016
|
+
child.once("error", (error) => void finish(null, error));
|
|
2017
|
+
child.once("close", (exitCode) => void finish(exitCode));
|
|
2018
|
+
if (options.timeoutMs !== void 0) {
|
|
2019
|
+
timeoutHandle = setTimeout(() => requestTermination("timeout"), options.timeoutMs);
|
|
2020
|
+
}
|
|
2021
|
+
if (options.signal) {
|
|
2022
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
2023
|
+
if (options.signal.aborted) onAbort();
|
|
2024
|
+
}
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
function openAnonymousEnvFile(content) {
|
|
2028
|
+
const dir = mkdtempSync2(join14(tmpdir2(), "bazilion-docker-env-"));
|
|
2029
|
+
const path = join14(dir, "environment");
|
|
2030
|
+
let fd;
|
|
2031
|
+
try {
|
|
2032
|
+
writeFileSync7(path, content, { encoding: "utf8", flag: "wx", mode: 384 });
|
|
2033
|
+
fd = openSync2(path, constants2.O_RDONLY | constants2.O_NOFOLLOW);
|
|
2034
|
+
rmSync10(dir, { recursive: true, force: true });
|
|
2035
|
+
return fd;
|
|
2036
|
+
} catch (error) {
|
|
2037
|
+
if (fd !== void 0) closeSync2(fd);
|
|
2038
|
+
rmSync10(dir, { recursive: true, force: true });
|
|
2039
|
+
throw error;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
function resolveTimeoutMs(timeout) {
|
|
2043
|
+
if (timeout === void 0) return void 0;
|
|
2044
|
+
if (!Number.isFinite(timeout) || timeout <= 0) {
|
|
2045
|
+
throw new Error("Invalid timeout: must be a finite number of seconds");
|
|
2046
|
+
}
|
|
2047
|
+
const timeoutMs = timeout * 1e3;
|
|
2048
|
+
if (timeoutMs > MAX_TIMEOUT_MS) {
|
|
2049
|
+
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_MS / 1e3} seconds`);
|
|
2050
|
+
}
|
|
2051
|
+
return timeoutMs;
|
|
2052
|
+
}
|
|
2053
|
+
async function resolveMountSource(cwd) {
|
|
2054
|
+
try {
|
|
2055
|
+
await access(cwd, constants2.F_OK);
|
|
2056
|
+
const info = await stat(cwd);
|
|
2057
|
+
if (!info.isDirectory()) {
|
|
2058
|
+
throw new Error(`Docker sandbox mount is not a directory: ${cwd}`);
|
|
2059
|
+
}
|
|
2060
|
+
return await realpath(cwd);
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
if (error instanceof Error && error.message.startsWith("Docker sandbox mount is not")) {
|
|
2063
|
+
throw error;
|
|
2064
|
+
}
|
|
2065
|
+
const code = error.code;
|
|
2066
|
+
if (code === "ENOENT") {
|
|
2067
|
+
throw new Error(`Docker sandbox mount does not exist: ${cwd}`);
|
|
2068
|
+
}
|
|
2069
|
+
throw new Error(
|
|
2070
|
+
`Docker sandbox cannot access mount "${cwd}": ${error instanceof Error ? error.message : String(error)}`
|
|
2071
|
+
);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
function hostIdentity() {
|
|
2075
|
+
if (typeof process.getuid !== "function" || typeof process.getgid !== "function") {
|
|
2076
|
+
throw new Error("Docker sandbox requires a host platform with numeric uid/gid support");
|
|
2077
|
+
}
|
|
2078
|
+
return { uid: process.getuid(), gid: process.getgid() };
|
|
2079
|
+
}
|
|
2080
|
+
function sortedEnvironment(env) {
|
|
2081
|
+
const entries = [];
|
|
2082
|
+
for (const [name, value] of Object.entries(env)) {
|
|
2083
|
+
if (value === void 0) continue;
|
|
2084
|
+
if (!ENV_NAME.test(name)) {
|
|
2085
|
+
throw new Error(
|
|
2086
|
+
`Docker sandbox environment variable name is invalid: ${JSON.stringify(name)}`
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
if (DOCKER_CLIENT_CONTROL_ENV2.has(name) && !CONTAINER_LITERAL_ENV_KEYS.has(name)) {
|
|
2090
|
+
throw new Error(`Docker sandbox cannot pass Docker client control variable: ${name}`);
|
|
2091
|
+
}
|
|
2092
|
+
if (SANDBOX_CONTROL_ENV2.has(name)) {
|
|
2093
|
+
throw new Error(`Docker sandbox cannot pass shell startup control variable: ${name}`);
|
|
2094
|
+
}
|
|
2095
|
+
if (value.includes("\0")) {
|
|
2096
|
+
throw new Error(`Docker sandbox environment variable ${name} contains a NUL byte`);
|
|
2097
|
+
}
|
|
2098
|
+
if (value.includes("\n") || value.includes("\r")) {
|
|
2099
|
+
throw new Error(`Docker sandbox environment variable ${name} cannot contain a line break`);
|
|
2100
|
+
}
|
|
2101
|
+
entries.push([name, value]);
|
|
2102
|
+
}
|
|
2103
|
+
return entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
2104
|
+
}
|
|
2105
|
+
async function resolveLocalDockerClientEnv(dockerPath, hostEnv) {
|
|
2106
|
+
const discoveryEnv = buildDockerClientEnv(hostEnv);
|
|
2107
|
+
const endpoint = await inspectDockerEndpoint(dockerPath, discoveryEnv);
|
|
2108
|
+
const prefix = "unix://";
|
|
2109
|
+
if (!endpoint.startsWith(prefix) || !isAbsolute2(endpoint.slice(prefix.length))) {
|
|
2110
|
+
throw new Error(
|
|
2111
|
+
`Docker sandbox requires a local Unix-socket context; active endpoint is ${JSON.stringify(endpoint)}`
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
const pinned = { DOCKER_HOST: endpoint };
|
|
2115
|
+
if (hostEnv.PATH !== void 0) pinned.PATH = hostEnv.PATH;
|
|
2116
|
+
if (hostEnv.HOME !== void 0) pinned.HOME = hostEnv.HOME;
|
|
2117
|
+
return pinned;
|
|
2118
|
+
}
|
|
2119
|
+
async function inspectDockerEndpoint(dockerPath, processEnv) {
|
|
2120
|
+
const raw = await runDockerInspection(
|
|
2121
|
+
dockerPath,
|
|
2122
|
+
["context", "inspect", "--format", "{{json .Endpoints.docker.Host}}"],
|
|
2123
|
+
processEnv,
|
|
2124
|
+
"the active context"
|
|
2125
|
+
);
|
|
2126
|
+
try {
|
|
2127
|
+
const parsed = JSON.parse(raw);
|
|
2128
|
+
if (typeof parsed !== "string" || parsed.length === 0) throw new Error("empty endpoint");
|
|
2129
|
+
return parsed;
|
|
2130
|
+
} catch {
|
|
2131
|
+
throw new Error(
|
|
2132
|
+
`Docker sandbox could not resolve the active Docker endpoint: ${raw || "empty context response"}`
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
async function inspectSandboxImage(dockerPath, image, processEnv) {
|
|
2137
|
+
let raw;
|
|
2138
|
+
try {
|
|
2139
|
+
raw = await runDockerInspection(
|
|
2140
|
+
dockerPath,
|
|
2141
|
+
["image", "inspect", "--format", '{{json .Id}} {{json (index .Config "Volumes")}}', image],
|
|
2142
|
+
processEnv,
|
|
2143
|
+
`local image ${JSON.stringify(image)}`
|
|
2144
|
+
);
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
const detail3 = error instanceof Error ? error.message : String(error);
|
|
2147
|
+
if (/no such image|unable to find image|manifest unknown/i.test(detail3)) {
|
|
2148
|
+
throw new Error(`Docker sandbox image "${image}" is unavailable: ${detail3}`);
|
|
2149
|
+
}
|
|
2150
|
+
throw error;
|
|
2151
|
+
}
|
|
2152
|
+
const separator = raw.indexOf(" ");
|
|
2153
|
+
if (separator < 0) {
|
|
2154
|
+
throw new Error(`Docker sandbox received an invalid inspection result for image "${image}"`);
|
|
2155
|
+
}
|
|
2156
|
+
let id;
|
|
2157
|
+
let volumes;
|
|
2158
|
+
try {
|
|
2159
|
+
id = JSON.parse(raw.slice(0, separator));
|
|
2160
|
+
volumes = JSON.parse(raw.slice(separator + 1));
|
|
2161
|
+
} catch {
|
|
2162
|
+
throw new Error(`Docker sandbox received an invalid inspection result for image "${image}"`);
|
|
2163
|
+
}
|
|
2164
|
+
if (typeof id !== "string" || !id.startsWith("sha256:")) {
|
|
2165
|
+
throw new Error(`Docker sandbox received an invalid image id for "${image}"`);
|
|
2166
|
+
}
|
|
2167
|
+
if (volumes !== null && typeof volumes === "object" && Object.keys(volumes).length > 0) {
|
|
2168
|
+
throw new Error(
|
|
2169
|
+
`Docker sandbox image "${image}" declares writable volumes (${Object.keys(volumes).sort().join(", ")}); use an image without VOLUME declarations`
|
|
2170
|
+
);
|
|
2171
|
+
}
|
|
2172
|
+
if (volumes !== null && (typeof volumes !== "object" || Array.isArray(volumes))) {
|
|
2173
|
+
throw new Error(`Docker sandbox received invalid volume metadata for image "${image}"`);
|
|
2174
|
+
}
|
|
2175
|
+
return id;
|
|
2176
|
+
}
|
|
2177
|
+
function runDockerInspection(dockerPath, args, processEnv, subject) {
|
|
2178
|
+
return new Promise((resolve5, reject) => {
|
|
2179
|
+
const child = spawn(dockerPath, args, {
|
|
2180
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2181
|
+
windowsHide: true,
|
|
2182
|
+
env: processEnv
|
|
2183
|
+
});
|
|
2184
|
+
let settled = false;
|
|
2185
|
+
let stdout = "";
|
|
2186
|
+
let stderr = "";
|
|
2187
|
+
const finish = (error) => {
|
|
2188
|
+
if (settled) return;
|
|
2189
|
+
settled = true;
|
|
2190
|
+
clearTimeout(timeoutHandle);
|
|
2191
|
+
if (error) {
|
|
2192
|
+
reject(error);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
resolve5(stdout.trim());
|
|
2196
|
+
};
|
|
2197
|
+
const timeoutHandle = setTimeout(() => {
|
|
2198
|
+
child.kill("SIGKILL");
|
|
2199
|
+
finish(
|
|
2200
|
+
new Error(
|
|
2201
|
+
`Docker sandbox could not inspect ${subject} within ${DOCKER_CONTEXT_TIMEOUT_MS}ms`
|
|
2202
|
+
)
|
|
2203
|
+
);
|
|
2204
|
+
}, DOCKER_CONTEXT_TIMEOUT_MS);
|
|
2205
|
+
child.stdout.on("data", (data) => {
|
|
2206
|
+
stdout = `${stdout}${data.toString("utf8")}`.slice(-4096);
|
|
2207
|
+
});
|
|
2208
|
+
child.stderr.on("data", (data) => {
|
|
2209
|
+
stderr = `${stderr}${data.toString("utf8")}`.slice(-4096);
|
|
2210
|
+
});
|
|
2211
|
+
child.once("error", (error) => finish(formatSpawnError(dockerPath, error)));
|
|
2212
|
+
child.once("close", (code) => {
|
|
2213
|
+
if (code === 0) finish();
|
|
2214
|
+
else
|
|
2215
|
+
finish(
|
|
2216
|
+
new Error(
|
|
2217
|
+
`Docker sandbox could not inspect ${subject}: ${stderr.trim() || `Docker exited with status ${code}`}`
|
|
2218
|
+
)
|
|
2219
|
+
);
|
|
2220
|
+
});
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
function buildDockerClientEnv(hostEnv) {
|
|
2224
|
+
const env = {};
|
|
2225
|
+
for (const key of DOCKER_CLIENT_ENV_KEYS) {
|
|
2226
|
+
const value = hostEnv[key];
|
|
2227
|
+
if (value !== void 0) env[key] = value;
|
|
2228
|
+
}
|
|
2229
|
+
return env;
|
|
2230
|
+
}
|
|
2231
|
+
function validateDockerPath(dockerPath) {
|
|
2232
|
+
if (!dockerPath.trim()) throw new Error("Docker sandbox executable path is required");
|
|
2233
|
+
if (dockerPath.includes("\0"))
|
|
2234
|
+
throw new Error("Docker sandbox executable path contains a NUL byte");
|
|
2235
|
+
}
|
|
2236
|
+
function validateImage(image) {
|
|
2237
|
+
if (!image.trim()) throw new Error("Docker sandbox image is required");
|
|
2238
|
+
if (image.startsWith("-")) {
|
|
2239
|
+
throw new Error(`Docker sandbox image cannot start with "-": ${JSON.stringify(image)}`);
|
|
2240
|
+
}
|
|
2241
|
+
if (image.includes("\0")) throw new Error("Docker sandbox image contains a NUL byte");
|
|
2242
|
+
}
|
|
2243
|
+
function validateContainerName(containerName) {
|
|
2244
|
+
if (!CONTAINER_NAME.test(containerName)) {
|
|
2245
|
+
throw new Error(`Docker sandbox container name is invalid: ${JSON.stringify(containerName)}`);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
function validateMountArgument(mountSource) {
|
|
2249
|
+
if (!mountSource) throw new Error("Docker sandbox mount path is required");
|
|
2250
|
+
if (mountSource.includes("\0") || mountSource.includes(",")) {
|
|
2251
|
+
throw new Error(
|
|
2252
|
+
`Docker sandbox mount path cannot be represented safely: ${JSON.stringify(mountSource)}`
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
function validateReadOnlyMounts(mounts, workspaceSource) {
|
|
2257
|
+
const targets = /* @__PURE__ */ new Set([CONTAINER_WORKDIR, "/tmp"]);
|
|
2258
|
+
for (const mount of mounts) {
|
|
2259
|
+
validateMountArgument(mount.source);
|
|
2260
|
+
if (mount.sourceRoot !== void 0 && (!isAbsolute2(mount.sourceRoot) || mount.sourceRoot.includes("\0") || mount.sourceRoot.includes(","))) {
|
|
2261
|
+
throw new Error(
|
|
2262
|
+
`Docker sandbox read-only mount root is invalid: ${JSON.stringify(mount.sourceRoot)}`
|
|
2263
|
+
);
|
|
2264
|
+
}
|
|
2265
|
+
if (!mount.target.startsWith("/") || mount.target.includes("\0") || mount.target.includes(",") || mount.target.split("/").includes("..")) {
|
|
2266
|
+
throw new Error(
|
|
2267
|
+
`Docker sandbox read-only mount target is invalid: ${JSON.stringify(mount.target)}`
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
if (mount.source === workspaceSource) {
|
|
2271
|
+
throw new Error("Docker sandbox cannot mount the writable workspace again as an input");
|
|
2272
|
+
}
|
|
2273
|
+
if (targets.has(mount.target)) {
|
|
2274
|
+
throw new Error(`Docker sandbox mount target is duplicated or reserved: ${mount.target}`);
|
|
2275
|
+
}
|
|
2276
|
+
targets.add(mount.target);
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
function isWithin(root, candidate) {
|
|
2280
|
+
const rel = relative2(root, candidate);
|
|
2281
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute2(rel);
|
|
2282
|
+
}
|
|
2283
|
+
function validateHostIdentity(uid, gid) {
|
|
2284
|
+
if (!Number.isSafeInteger(uid) || uid < 0 || !Number.isSafeInteger(gid) || gid < 0) {
|
|
2285
|
+
throw new Error(`Docker sandbox received an invalid host uid/gid: ${uid}:${gid}`);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
function formatSpawnError(dockerPath, error) {
|
|
2289
|
+
const code = error.code;
|
|
2290
|
+
if (code === "ENOENT") {
|
|
2291
|
+
return new Error(`Docker sandbox unavailable: executable "${dockerPath}" was not found`);
|
|
2292
|
+
}
|
|
2293
|
+
if (code === "EACCES") {
|
|
2294
|
+
return new Error(`Docker sandbox unavailable: executable "${dockerPath}" is not executable`);
|
|
2295
|
+
}
|
|
2296
|
+
return new Error(`Docker sandbox could not start "${dockerPath}": ${error.message}`);
|
|
2297
|
+
}
|
|
2298
|
+
function formatDockerRunError(spec, stderr) {
|
|
2299
|
+
const detail3 = stderr.trim() || "Docker exited with status 125 without an error message";
|
|
2300
|
+
if (/no such image|unable to find image|manifest unknown|pull access denied/i.test(detail3)) {
|
|
2301
|
+
return new Error(`Docker sandbox image "${spec.image}" is unavailable: ${detail3}`);
|
|
2302
|
+
}
|
|
2303
|
+
if (/mounts denied|invalid mount config|bind source path does not exist|mount.*failed/i.test(detail3)) {
|
|
2304
|
+
return new Error(`Docker sandbox mount failed for "${spec.mountSource}": ${detail3}`);
|
|
2305
|
+
}
|
|
2306
|
+
if (/cannot connect to the docker daemon|is the docker daemon running/i.test(detail3)) {
|
|
2307
|
+
return new Error(`Docker sandbox unavailable: ${detail3}`);
|
|
2308
|
+
}
|
|
2309
|
+
return new Error(`Docker sandbox failed to start container "${spec.containerName}": ${detail3}`);
|
|
2310
|
+
}
|
|
2311
|
+
async function removeContainer(dockerPath, containerName, processEnv) {
|
|
2312
|
+
const args = ["container", "rm", "--force", containerName];
|
|
2313
|
+
let last;
|
|
2314
|
+
for (let attempt = 0; attempt < CLEANUP_ATTEMPTS; attempt += 1) {
|
|
2315
|
+
last = await runCleanupCommand(dockerPath, args, processEnv);
|
|
2316
|
+
if (last.ok) return;
|
|
2317
|
+
if (attempt < CLEANUP_ATTEMPTS - 1) {
|
|
2318
|
+
await new Promise((resolve5) => setTimeout(resolve5, CLEANUP_RETRY_DELAY_MS));
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
if (last && !last.missing) {
|
|
2322
|
+
console.warn(
|
|
2323
|
+
`Docker sandbox could not confirm cleanup of container "${containerName}": ${last.error || "cleanup command failed"}`
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
function runCleanupCommand(dockerPath, args, processEnv) {
|
|
2328
|
+
return new Promise((resolve5) => {
|
|
2329
|
+
let done = false;
|
|
2330
|
+
let stderr = "";
|
|
2331
|
+
const child = spawn(dockerPath, args, {
|
|
2332
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
2333
|
+
windowsHide: true,
|
|
2334
|
+
env: processEnv
|
|
2335
|
+
});
|
|
2336
|
+
const finish = (ok, error = "") => {
|
|
2337
|
+
if (done) return;
|
|
2338
|
+
done = true;
|
|
2339
|
+
clearTimeout(timeoutHandle);
|
|
2340
|
+
const detail3 = error || stderr.trim();
|
|
2341
|
+
resolve5({
|
|
2342
|
+
ok,
|
|
2343
|
+
missing: /no such container/i.test(detail3),
|
|
2344
|
+
error: detail3
|
|
2345
|
+
});
|
|
2346
|
+
};
|
|
2347
|
+
const timeoutHandle = setTimeout(() => {
|
|
2348
|
+
child.kill("SIGKILL");
|
|
2349
|
+
finish(false, `cleanup timed out after ${CLEANUP_TIMEOUT_MS}ms`);
|
|
2350
|
+
}, CLEANUP_TIMEOUT_MS);
|
|
2351
|
+
child.stderr.on("data", (data) => {
|
|
2352
|
+
stderr = `${stderr}${data.toString("utf8")}`.slice(-2048);
|
|
2353
|
+
});
|
|
2354
|
+
child.once("error", (error) => finish(false, error.message));
|
|
2355
|
+
child.once("close", (code) => finish(code === 0));
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
// ../daemon/src/runtime/shell/tooling.ts
|
|
2360
|
+
var HOST_CODING_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
2361
|
+
var HOST_FILE_TOOL_NAMES = ["read", "edit", "write", "grep", "find", "ls"];
|
|
2362
|
+
var CONTAINER_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
|
|
2363
|
+
var SANDBOX_INPUTS_DIR = "/inputs";
|
|
2364
|
+
var SANDBOX_MEMORY_DIR = "/workspace/memory";
|
|
2365
|
+
function buildSandboxContainerEnv(env, allowlist) {
|
|
2366
|
+
return {
|
|
2367
|
+
...buildScrubbedShellEnv(env, allowlist),
|
|
2368
|
+
PATH: CONTAINER_PATH,
|
|
2369
|
+
HOME: "/tmp",
|
|
2370
|
+
LANG: "C.UTF-8",
|
|
2371
|
+
LC_ALL: "C.UTF-8",
|
|
2372
|
+
SHELL: "/bin/bash",
|
|
2373
|
+
TMPDIR: "/tmp"
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
function createSessionShellTools(cwd, env, options = {}) {
|
|
2377
|
+
const config = resolveShellSecurityConfig(env);
|
|
2378
|
+
if (config.sandboxMode === "off" && config.approvalMode === "off") {
|
|
2379
|
+
return { config, hostToolNames: HOST_CODING_TOOL_NAMES };
|
|
2380
|
+
}
|
|
2381
|
+
if (config.sandboxMode === "off") {
|
|
2382
|
+
const base2 = createBashToolDefinition(cwd, {
|
|
2383
|
+
operations: createLocalBashOperations()
|
|
2384
|
+
});
|
|
2385
|
+
const customBash2 = createApprovalGatedBashTool(base2, options.approvalHost);
|
|
2386
|
+
return { config, hostToolNames: HOST_FILE_TOOL_NAMES, customBash: customBash2 };
|
|
2387
|
+
}
|
|
2388
|
+
const operations = createDockerBashOperations({
|
|
2389
|
+
image: config.sandboxImage,
|
|
2390
|
+
env: buildSandboxContainerEnv(env, config.envAllowlist),
|
|
2391
|
+
readOnlyMounts: [
|
|
2392
|
+
...existsSync11(join15(cwd, "memory")) ? [safeReadOnlyMount(join15(cwd, "memory"), SANDBOX_MEMORY_DIR, cwd)] : [],
|
|
2393
|
+
...options.inputsDir ? [safeReadOnlyMount(options.inputsDir, SANDBOX_INPUTS_DIR, dirname3(options.inputsDir))] : [],
|
|
2394
|
+
...(options.skillMounts ?? []).map(
|
|
2395
|
+
(mount) => safeReadOnlyMount(mount.source, mount.target, dirname3(mount.source))
|
|
2396
|
+
)
|
|
2397
|
+
]
|
|
2398
|
+
});
|
|
2399
|
+
const base = createBashToolDefinition(cwd, { operations });
|
|
2400
|
+
const dockerBash = defineTool2({
|
|
2401
|
+
...base,
|
|
2402
|
+
label: "bash (Docker sandbox)",
|
|
2403
|
+
description: `${base.description} Runs in an ephemeral, network-disabled Docker container with the team workspace mounted read/write plus only bounded read-only memory, skill, and attachment mounts. Other host files and host credentials are unavailable.`
|
|
2404
|
+
});
|
|
2405
|
+
const customBash = config.approvalMode === "dangerous" ? createApprovalGatedBashTool(dockerBash, options.approvalHost) : dockerBash;
|
|
2406
|
+
return { config, hostToolNames: [], customBash };
|
|
2407
|
+
}
|
|
2408
|
+
function safeReadOnlyMount(source, target, expectedRoot) {
|
|
2409
|
+
const info = lstatSync2(source);
|
|
2410
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
2411
|
+
throw new Error(`Docker sandbox read-only mount must be a real directory: ${source}`);
|
|
2412
|
+
}
|
|
2413
|
+
const sourceRoot = realpathSync2(expectedRoot);
|
|
2414
|
+
const canonicalSource = realpathSync2(source);
|
|
2415
|
+
const rel = relative3(sourceRoot, canonicalSource);
|
|
2416
|
+
const contained = rel === "" || rel !== ".." && !rel.startsWith(`..${sep4}`) && !isAbsolute3(rel);
|
|
2417
|
+
if (!contained) {
|
|
2418
|
+
throw new Error(`Docker sandbox read-only mount escaped its configured root: ${source}`);
|
|
2419
|
+
}
|
|
2420
|
+
return { source: canonicalSource, target, sourceRoot };
|
|
2421
|
+
}
|
|
1068
2422
|
|
|
1069
2423
|
// ../daemon/src/runtime/pi/tools.ts
|
|
1070
2424
|
import { Type as Type2 } from "typebox";
|
|
1071
2425
|
|
|
1072
2426
|
// ../daemon/src/runtime/tools/bootstrap.ts
|
|
1073
|
-
import { existsSync as existsSync12, rmSync as
|
|
1074
|
-
import { join as
|
|
2427
|
+
import { existsSync as existsSync12, rmSync as rmSync11 } from "fs";
|
|
2428
|
+
import { join as join16 } from "path";
|
|
1075
2429
|
function bootstrapTool(agentDir) {
|
|
1076
2430
|
return {
|
|
1077
2431
|
def: {
|
|
@@ -1080,9 +2434,9 @@ function bootstrapTool(agentDir) {
|
|
|
1080
2434
|
parameters: { type: "object", properties: {} }
|
|
1081
2435
|
},
|
|
1082
2436
|
async invoke() {
|
|
1083
|
-
const path =
|
|
2437
|
+
const path = join16(agentDir, "BOOTSTRAP.md");
|
|
1084
2438
|
if (existsSync12(path)) {
|
|
1085
|
-
|
|
2439
|
+
rmSync11(path);
|
|
1086
2440
|
return "BOOTSTRAP.md removed. Bootstrap is complete.";
|
|
1087
2441
|
}
|
|
1088
2442
|
return "BOOTSTRAP.md was already removed.";
|
|
@@ -1220,8 +2574,16 @@ function browserTools(host, agentId) {
|
|
|
1220
2574
|
}
|
|
1221
2575
|
|
|
1222
2576
|
// ../daemon/src/runtime/tools/deliver-file.ts
|
|
1223
|
-
import {
|
|
1224
|
-
|
|
2577
|
+
import {
|
|
2578
|
+
closeSync as closeSync3,
|
|
2579
|
+
constants as constants3,
|
|
2580
|
+
fstatSync as fstatSync2,
|
|
2581
|
+
openSync as openSync3,
|
|
2582
|
+
readFileSync as readFileSync10,
|
|
2583
|
+
realpathSync as realpathSync3,
|
|
2584
|
+
statSync as statSync3
|
|
2585
|
+
} from "fs";
|
|
2586
|
+
import { basename as basename2, extname, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve4, sep as sep5 } from "path";
|
|
1225
2587
|
var MAX_DELIVER_BYTES = 25 * 1024 * 1024;
|
|
1226
2588
|
var MIME = {
|
|
1227
2589
|
".pdf": "application/pdf",
|
|
@@ -1237,7 +2599,17 @@ var MIME = {
|
|
|
1237
2599
|
".gif": "image/gif",
|
|
1238
2600
|
".webp": "image/webp"
|
|
1239
2601
|
};
|
|
2602
|
+
function isWithin2(root, candidate) {
|
|
2603
|
+
const rel = relative4(root, candidate);
|
|
2604
|
+
return rel === "" || rel !== ".." && !rel.startsWith(`..${sep5}`) && !isAbsolute4(rel);
|
|
2605
|
+
}
|
|
1240
2606
|
function deliverFileTool(cwd, sink) {
|
|
2607
|
+
const workspacePath = resolve4(cwd);
|
|
2608
|
+
let workspaceRealPath = null;
|
|
2609
|
+
try {
|
|
2610
|
+
workspaceRealPath = realpathSync3(workspacePath);
|
|
2611
|
+
} catch {
|
|
2612
|
+
}
|
|
1241
2613
|
return {
|
|
1242
2614
|
def: {
|
|
1243
2615
|
name: "deliver_file",
|
|
@@ -1247,7 +2619,7 @@ function deliverFileTool(cwd, sink) {
|
|
|
1247
2619
|
properties: {
|
|
1248
2620
|
path: {
|
|
1249
2621
|
type: "string",
|
|
1250
|
-
description: "Path to
|
|
2622
|
+
description: "Path to a file, relative to your workspace."
|
|
1251
2623
|
}
|
|
1252
2624
|
},
|
|
1253
2625
|
required: ["path"],
|
|
@@ -1257,29 +2629,71 @@ function deliverFileTool(cwd, sink) {
|
|
|
1257
2629
|
async invoke(args) {
|
|
1258
2630
|
const p = String(args.path ?? "");
|
|
1259
2631
|
if (!p) throw new Error("deliver_file: path is required");
|
|
1260
|
-
|
|
1261
|
-
|
|
2632
|
+
if (isAbsolute4(p)) {
|
|
2633
|
+
throw new Error(`deliver_file: path must stay within the workspace: ${p}`);
|
|
2634
|
+
}
|
|
2635
|
+
const abs = resolve4(workspacePath, p);
|
|
2636
|
+
if (!isWithin2(workspacePath, abs)) {
|
|
2637
|
+
throw new Error(`deliver_file: path must stay within the workspace: ${p}`);
|
|
2638
|
+
}
|
|
2639
|
+
if (!workspaceRealPath) {
|
|
2640
|
+
throw new Error("deliver_file: workspace is unavailable");
|
|
2641
|
+
}
|
|
2642
|
+
let real;
|
|
2643
|
+
let expected;
|
|
1262
2644
|
try {
|
|
1263
|
-
|
|
2645
|
+
real = realpathSync3(abs);
|
|
2646
|
+
expected = statSync3(real);
|
|
1264
2647
|
} catch {
|
|
1265
2648
|
throw new Error(`deliver_file: no such file: ${p}`);
|
|
1266
2649
|
}
|
|
1267
|
-
if (
|
|
1268
|
-
throw new Error(
|
|
1269
|
-
|
|
1270
|
-
|
|
2650
|
+
if (!isWithin2(workspaceRealPath, real)) {
|
|
2651
|
+
throw new Error(`deliver_file: path must stay within the workspace: ${p}`);
|
|
2652
|
+
}
|
|
2653
|
+
let fd;
|
|
2654
|
+
try {
|
|
2655
|
+
fd = openSync3(real, constants3.O_RDONLY | constants3.O_NOFOLLOW | constants3.O_NONBLOCK);
|
|
2656
|
+
} catch {
|
|
2657
|
+
throw new Error(`deliver_file: no such file: ${p}`);
|
|
2658
|
+
}
|
|
2659
|
+
let data;
|
|
2660
|
+
try {
|
|
2661
|
+
const stat2 = fstatSync2(fd);
|
|
2662
|
+
if (!stat2.isFile()) {
|
|
2663
|
+
throw new Error(`deliver_file: not a regular file: ${p}`);
|
|
2664
|
+
}
|
|
2665
|
+
if (stat2.dev !== expected.dev || stat2.ino !== expected.ino) {
|
|
2666
|
+
throw new Error(`deliver_file: path changed during validation: ${p}`);
|
|
2667
|
+
}
|
|
2668
|
+
let confirmedReal;
|
|
2669
|
+
try {
|
|
2670
|
+
confirmedReal = realpathSync3(abs);
|
|
2671
|
+
} catch {
|
|
2672
|
+
throw new Error(`deliver_file: path changed during validation: ${p}`);
|
|
2673
|
+
}
|
|
2674
|
+
if (confirmedReal !== real) {
|
|
2675
|
+
throw new Error(`deliver_file: path changed during validation: ${p}`);
|
|
2676
|
+
}
|
|
2677
|
+
if (stat2.size > MAX_DELIVER_BYTES) {
|
|
2678
|
+
throw new Error(
|
|
2679
|
+
`deliver_file: "${basename2(abs)}" is too large (${(stat2.size / 1024 / 1024).toFixed(1)} MB > 25 MB)`
|
|
2680
|
+
);
|
|
2681
|
+
}
|
|
2682
|
+
data = readFileSync10(fd).toString("base64");
|
|
2683
|
+
} finally {
|
|
2684
|
+
closeSync3(fd);
|
|
1271
2685
|
}
|
|
1272
2686
|
const name = basename2(abs);
|
|
1273
2687
|
const mimeType = MIME[extname(abs).toLowerCase()] ?? "application/octet-stream";
|
|
1274
|
-
sink({ name, mimeType, data
|
|
2688
|
+
sink({ name, mimeType, data });
|
|
1275
2689
|
return `Delivered "${name}" (${mimeType}) to the user.`;
|
|
1276
2690
|
}
|
|
1277
2691
|
};
|
|
1278
2692
|
}
|
|
1279
2693
|
|
|
1280
2694
|
// ../daemon/src/runtime/tools/home.ts
|
|
1281
|
-
import { readdirSync as
|
|
1282
|
-
import { join as
|
|
2695
|
+
import { readdirSync as readdirSync7, readFileSync as readFileSync11, statSync as statSync4, writeFileSync as writeFileSync8 } from "fs";
|
|
2696
|
+
import { join as join17 } from "path";
|
|
1283
2697
|
var HOME_FILES_READABLE = [
|
|
1284
2698
|
"IDENTITY.md",
|
|
1285
2699
|
"SOUL.md",
|
|
@@ -1309,7 +2723,7 @@ function homeTools(agentDir) {
|
|
|
1309
2723
|
`home_read: "file" must be one of ${HOME_FILES_READABLE.join(", ")}; got "${file}"`
|
|
1310
2724
|
);
|
|
1311
2725
|
}
|
|
1312
|
-
const path =
|
|
2726
|
+
const path = join17(agentDir, file);
|
|
1313
2727
|
try {
|
|
1314
2728
|
return readFileSync11(path, "utf8");
|
|
1315
2729
|
} catch (err) {
|
|
@@ -1339,8 +2753,8 @@ function homeTools(agentDir) {
|
|
|
1339
2753
|
);
|
|
1340
2754
|
}
|
|
1341
2755
|
const content = typeof args.content === "string" ? args.content : "";
|
|
1342
|
-
const path =
|
|
1343
|
-
|
|
2756
|
+
const path = join17(agentDir, file);
|
|
2757
|
+
writeFileSync8(path, content, "utf8");
|
|
1344
2758
|
return `wrote ${file} (${Buffer.byteLength(content, "utf8")} bytes)`;
|
|
1345
2759
|
}
|
|
1346
2760
|
},
|
|
@@ -1353,9 +2767,9 @@ function homeTools(agentDir) {
|
|
|
1353
2767
|
async invoke() {
|
|
1354
2768
|
const entries = [];
|
|
1355
2769
|
for (const file of HOME_FILES_READABLE) {
|
|
1356
|
-
const path =
|
|
2770
|
+
const path = join17(agentDir, file);
|
|
1357
2771
|
try {
|
|
1358
|
-
const s =
|
|
2772
|
+
const s = statSync4(path);
|
|
1359
2773
|
entries.push(`${file} (${s.size}b)`);
|
|
1360
2774
|
} catch {
|
|
1361
2775
|
}
|
|
@@ -1363,7 +2777,7 @@ function homeTools(agentDir) {
|
|
|
1363
2777
|
if (entries.length === 0) {
|
|
1364
2778
|
const dirEntries = (() => {
|
|
1365
2779
|
try {
|
|
1366
|
-
return
|
|
2780
|
+
return readdirSync7(agentDir);
|
|
1367
2781
|
} catch {
|
|
1368
2782
|
return [];
|
|
1369
2783
|
}
|
|
@@ -2278,7 +3692,6 @@ function createBazilionCustomTools(opts) {
|
|
|
2278
3692
|
}
|
|
2279
3693
|
|
|
2280
3694
|
// ../daemon/src/runtime/pi/session.ts
|
|
2281
|
-
var BUILTIN_TOOL_NAMES = ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
2282
3695
|
async function createBazilionSession(opts) {
|
|
2283
3696
|
const {
|
|
2284
3697
|
agent,
|
|
@@ -2292,7 +3705,9 @@ async function createBazilionSession(opts) {
|
|
|
2292
3705
|
mcpHost,
|
|
2293
3706
|
mcpTools,
|
|
2294
3707
|
fileSink,
|
|
2295
|
-
|
|
3708
|
+
bashApprovalHost,
|
|
3709
|
+
refreshApiKey,
|
|
3710
|
+
restricted
|
|
2296
3711
|
} = opts;
|
|
2297
3712
|
const { providerName, modelId } = splitModelString(agent.model);
|
|
2298
3713
|
if (enabledProviders.size > 0 && !enabledProviders.has(providerName)) {
|
|
@@ -2324,9 +3739,19 @@ async function createBazilionSession(opts) {
|
|
|
2324
3739
|
}
|
|
2325
3740
|
const cwd = agent.team.path;
|
|
2326
3741
|
if (!existsSync13(cwd)) mkdirSync5(cwd, { recursive: true });
|
|
2327
|
-
const
|
|
3742
|
+
const promptSkills = restricted ? [] : loadPromptSkills(paths.skillsDir, agent.skills);
|
|
3743
|
+
const uploadsDir = join18(agent.agent.dir, "uploads");
|
|
3744
|
+
const shellTools = restricted ? null : createSessionShellTools(cwd, env, {
|
|
3745
|
+
...existsSync13(uploadsDir) ? { inputsDir: uploadsDir } : {},
|
|
3746
|
+
skillMounts: promptSkills.map((skill) => ({
|
|
3747
|
+
source: skill.hostDir,
|
|
3748
|
+
target: skill.sandboxDir
|
|
3749
|
+
})),
|
|
3750
|
+
approvalHost: bashApprovalHost
|
|
3751
|
+
});
|
|
3752
|
+
const sessionDir = restricted?.sessionDir ?? join18(paths.agentDir(agent.agent.id), "sessions");
|
|
2328
3753
|
mkdirSync5(sessionDir, { recursive: true });
|
|
2329
|
-
const existing = findMostRecent(sessionDir);
|
|
3754
|
+
const existing = restricted ? null : findMostRecent(sessionDir);
|
|
2330
3755
|
const sessionManager = existing ? SessionManager.open(existing, sessionDir, cwd) : SessionManager.create(cwd, sessionDir);
|
|
2331
3756
|
const settingsManager = SettingsManager.inMemory({
|
|
2332
3757
|
compaction: { enabled: false },
|
|
@@ -2337,10 +3762,13 @@ async function createBazilionSession(opts) {
|
|
|
2337
3762
|
provider: { maxRetryDelayMs: 8e3 }
|
|
2338
3763
|
}
|
|
2339
3764
|
});
|
|
2340
|
-
const bazilionPrompt = buildSystemPrompt(agent
|
|
3765
|
+
const bazilionPrompt = restricted?.systemPrompt ?? buildSystemPrompt(agent, {
|
|
3766
|
+
skills: promptSkills,
|
|
3767
|
+
sandboxMode: shellTools?.config.sandboxMode ?? "off"
|
|
3768
|
+
});
|
|
2341
3769
|
const resourceLoader = createBazilionResourceLoader(bazilionPrompt);
|
|
2342
3770
|
await resourceLoader.reload();
|
|
2343
|
-
const
|
|
3771
|
+
const bazilionTools = restricted ? restricted.tools : createBazilionCustomTools({
|
|
2344
3772
|
agent,
|
|
2345
3773
|
memory,
|
|
2346
3774
|
messagingHost,
|
|
@@ -2351,12 +3779,13 @@ async function createBazilionSession(opts) {
|
|
|
2351
3779
|
fileSink,
|
|
2352
3780
|
env
|
|
2353
3781
|
});
|
|
2354
|
-
const
|
|
3782
|
+
const customTools = shellTools?.customBash ? [...bazilionTools, shellTools.customBash] : bazilionTools;
|
|
3783
|
+
const allowedTools = [...shellTools?.hostToolNames ?? [], ...customTools.map((t) => t.name)];
|
|
2355
3784
|
const { session } = await createAgentSession({
|
|
2356
3785
|
cwd,
|
|
2357
|
-
agentDir:
|
|
3786
|
+
agentDir: join18(paths.home, "pi"),
|
|
2358
3787
|
model,
|
|
2359
|
-
thinkingLevel: toPiThinkingLevel(agent.reasoningLevel),
|
|
3788
|
+
thinkingLevel: toPiThinkingLevel(restricted?.reasoningLevel ?? agent.reasoningLevel),
|
|
2360
3789
|
tools: allowedTools,
|
|
2361
3790
|
customTools,
|
|
2362
3791
|
sessionManager,
|
|
@@ -2482,11 +3911,11 @@ function createBazilionResourceLoader(appendSystemPrompt) {
|
|
|
2482
3911
|
function findMostRecent(sessionDir) {
|
|
2483
3912
|
if (!existsSync13(sessionDir)) return null;
|
|
2484
3913
|
let newest = null;
|
|
2485
|
-
for (const entry of
|
|
3914
|
+
for (const entry of readdirSync8(sessionDir)) {
|
|
2486
3915
|
if (!entry.endsWith(".jsonl")) continue;
|
|
2487
|
-
const path =
|
|
3916
|
+
const path = join18(sessionDir, entry);
|
|
2488
3917
|
try {
|
|
2489
|
-
const s =
|
|
3918
|
+
const s = statSync5(path);
|
|
2490
3919
|
if (!newest || s.mtimeMs > newest.mtimeMs) newest = { path, mtimeMs: s.mtimeMs };
|
|
2491
3920
|
} catch {
|
|
2492
3921
|
}
|
|
@@ -2494,53 +3923,99 @@ function findMostRecent(sessionDir) {
|
|
|
2494
3923
|
return newest?.path ?? null;
|
|
2495
3924
|
}
|
|
2496
3925
|
|
|
2497
|
-
// ../daemon/src/runtime/worker/
|
|
2498
|
-
function
|
|
2499
|
-
|
|
2500
|
-
|
|
3926
|
+
// ../daemon/src/runtime/worker/api-key-refresh.ts
|
|
3927
|
+
function createIpcApiKeyRefresher(call, context) {
|
|
3928
|
+
return async (providerName) => {
|
|
3929
|
+
assertOpenAICodexProvider(providerName, context.providerName);
|
|
3930
|
+
return call("refreshApiKey", {
|
|
3931
|
+
providerName,
|
|
3932
|
+
agentId: context.agentId,
|
|
3933
|
+
turnId: context.turnId
|
|
3934
|
+
});
|
|
3935
|
+
};
|
|
2501
3936
|
}
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
2506
|
-
if (!raw) throw new Error("worker: empty stdin input");
|
|
2507
|
-
const parsed = JSON.parse(raw);
|
|
2508
|
-
if (!parsed.agent || typeof parsed.message !== "string" || !Array.isArray(parsed.enabledProviders)) {
|
|
2509
|
-
throw new Error("worker: stdin must be {agent, message, enabledProviders}");
|
|
3937
|
+
function assertOpenAICodexProvider(requested, expected) {
|
|
3938
|
+
if (expected !== "openai-codex" || requested !== expected) {
|
|
3939
|
+
throw new Error("unexpected API key refresh provider");
|
|
2510
3940
|
}
|
|
2511
|
-
return parsed;
|
|
2512
3941
|
}
|
|
2513
|
-
|
|
3942
|
+
|
|
3943
|
+
// ../daemon/src/runtime/worker/ipc-client.ts
|
|
3944
|
+
import { randomUUID as randomUUID18 } from "crypto";
|
|
3945
|
+
function createIpcClient(channel) {
|
|
2514
3946
|
const pending = /* @__PURE__ */ new Map();
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
3947
|
+
let disconnected = false;
|
|
3948
|
+
channel.onMessage((message) => {
|
|
3949
|
+
if (!isIpcReply(message)) return;
|
|
3950
|
+
const entry = pending.get(message.id);
|
|
2518
3951
|
if (!entry) return;
|
|
2519
|
-
pending.delete(
|
|
2520
|
-
if (
|
|
2521
|
-
else entry.reject(new Error(
|
|
3952
|
+
pending.delete(message.id);
|
|
3953
|
+
if (message.ok) entry.resolve(message.result);
|
|
3954
|
+
else entry.reject(new Error(message.error));
|
|
2522
3955
|
});
|
|
2523
|
-
|
|
2524
|
-
|
|
3956
|
+
channel.onDisconnect(() => {
|
|
3957
|
+
disconnected = true;
|
|
3958
|
+
for (const entry of pending.values()) {
|
|
3959
|
+
entry.reject(new Error("worker IPC channel disconnected"));
|
|
3960
|
+
}
|
|
2525
3961
|
pending.clear();
|
|
2526
3962
|
});
|
|
2527
3963
|
return (method, args) => {
|
|
2528
|
-
if (
|
|
3964
|
+
if (disconnected) {
|
|
3965
|
+
return Promise.reject(new Error("worker IPC channel disconnected"));
|
|
3966
|
+
}
|
|
3967
|
+
const send = channel.send;
|
|
3968
|
+
if (!send) {
|
|
2529
3969
|
return Promise.reject(new Error("worker: no IPC channel \u2014 daemon must spawn with stdio:ipc"));
|
|
2530
3970
|
}
|
|
2531
|
-
const id =
|
|
3971
|
+
const id = randomUUID18();
|
|
2532
3972
|
const message = { type: "rpc", id, method, args };
|
|
2533
|
-
return new Promise((
|
|
2534
|
-
pending.set(id, { resolve: (
|
|
2535
|
-
|
|
2536
|
-
|
|
3973
|
+
return new Promise((resolve5, reject) => {
|
|
3974
|
+
pending.set(id, { resolve: (value) => resolve5(value), reject });
|
|
3975
|
+
try {
|
|
3976
|
+
send(message, (error) => {
|
|
3977
|
+
if (!error) return;
|
|
2537
3978
|
pending.delete(id);
|
|
2538
|
-
reject(
|
|
2539
|
-
}
|
|
2540
|
-
})
|
|
3979
|
+
reject(error);
|
|
3980
|
+
});
|
|
3981
|
+
} catch (error) {
|
|
3982
|
+
pending.delete(id);
|
|
3983
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
3984
|
+
}
|
|
2541
3985
|
});
|
|
2542
3986
|
};
|
|
2543
3987
|
}
|
|
3988
|
+
function isIpcReply(message) {
|
|
3989
|
+
if (!message || typeof message !== "object") return false;
|
|
3990
|
+
const candidate = message;
|
|
3991
|
+
return candidate.type === "rpc-reply" && typeof candidate.id === "string" && typeof candidate.ok === "boolean";
|
|
3992
|
+
}
|
|
3993
|
+
|
|
3994
|
+
// ../daemon/src/runtime/worker/entry.ts
|
|
3995
|
+
function emit(frame) {
|
|
3996
|
+
process.stdout.write(`${JSON.stringify(frame)}
|
|
3997
|
+
`);
|
|
3998
|
+
}
|
|
3999
|
+
async function readInput() {
|
|
4000
|
+
const chunks = [];
|
|
4001
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
4002
|
+
const raw = Buffer.concat(chunks).toString("utf8").trim();
|
|
4003
|
+
if (!raw) throw new Error("worker: empty stdin input");
|
|
4004
|
+
const parsed = JSON.parse(raw);
|
|
4005
|
+
if (!parsed.agent || typeof parsed.message !== "string" || !Array.isArray(parsed.enabledProviders) || typeof parsed.turnId !== "string" || parsed.apiKeyRefreshEnabled !== void 0 && typeof parsed.apiKeyRefreshEnabled !== "boolean" || parsed.bashApprovalMode !== "interactive" && parsed.bashApprovalMode !== "auto_deny") {
|
|
4006
|
+
throw new Error(
|
|
4007
|
+
"worker: stdin must include {agent, message, enabledProviders, turnId, bashApprovalMode}"
|
|
4008
|
+
);
|
|
4009
|
+
}
|
|
4010
|
+
if (parsed.mode === "review" && !parsed.review?.reviewId) {
|
|
4011
|
+
throw new Error("worker: review mode requires review metadata");
|
|
4012
|
+
}
|
|
4013
|
+
const providerName = parsed.agent.model.split(":", 1)[0] ?? "";
|
|
4014
|
+
if (parsed.apiKeyRefreshEnabled && (providerName !== "openai-codex" || typeof parsed.apiKey !== "string")) {
|
|
4015
|
+
throw new Error("worker: API key refresh requires an openai-codex turn with an initial token");
|
|
4016
|
+
}
|
|
4017
|
+
return parsed;
|
|
4018
|
+
}
|
|
2544
4019
|
function createIpcMessagingHost(call) {
|
|
2545
4020
|
return {
|
|
2546
4021
|
agentExists: (agentId) => call("agentExists", { agentId }),
|
|
@@ -2569,10 +4044,22 @@ function createIpcMcpHost(call) {
|
|
|
2569
4044
|
invoke: (serverId, toolName, args) => call("mcpInvoke", { serverId, toolName, args })
|
|
2570
4045
|
};
|
|
2571
4046
|
}
|
|
2572
|
-
function
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
4047
|
+
function createIpcBashApprovalHost(call, input) {
|
|
4048
|
+
return {
|
|
4049
|
+
async requestApproval(request) {
|
|
4050
|
+
const result = await call("bashApproval", {
|
|
4051
|
+
id: randomUUID19(),
|
|
4052
|
+
turnId: input.turnId,
|
|
4053
|
+
toolCallId: request.toolCallId,
|
|
4054
|
+
agentId: input.agent.agent.id,
|
|
4055
|
+
teamId: input.agent.team.id,
|
|
4056
|
+
command: request.command,
|
|
4057
|
+
risks: [...request.risks],
|
|
4058
|
+
mode: input.bashApprovalMode
|
|
4059
|
+
});
|
|
4060
|
+
return result.decision === "allow" ? "approved" : "denied";
|
|
4061
|
+
}
|
|
4062
|
+
};
|
|
2576
4063
|
}
|
|
2577
4064
|
async function main() {
|
|
2578
4065
|
let aborted = false;
|
|
@@ -2591,15 +4078,90 @@ async function main() {
|
|
|
2591
4078
|
};
|
|
2592
4079
|
process.on("SIGTERM", onSignal);
|
|
2593
4080
|
process.on("SIGINT", onSignal);
|
|
2594
|
-
const {
|
|
4081
|
+
const {
|
|
4082
|
+
agent,
|
|
4083
|
+
message,
|
|
4084
|
+
enabledProviders,
|
|
4085
|
+
apiKey,
|
|
4086
|
+
apiKeyRefreshEnabled,
|
|
4087
|
+
browserEnabled,
|
|
4088
|
+
mcpTools,
|
|
4089
|
+
images,
|
|
4090
|
+
turnId,
|
|
4091
|
+
bashApprovalMode,
|
|
4092
|
+
mode = "chat",
|
|
4093
|
+
review
|
|
4094
|
+
} = await readInput();
|
|
2595
4095
|
const paths = resolvePaths();
|
|
2596
|
-
const memory = qmdBackend(
|
|
4096
|
+
const memory = qmdBackend(join19(agent.team.path, "memory"));
|
|
2597
4097
|
await memory.init();
|
|
2598
|
-
const ipcCall = createIpcClient(
|
|
4098
|
+
const ipcCall = createIpcClient({
|
|
4099
|
+
send: process.send ? (ipcMessage, done) => {
|
|
4100
|
+
process.send?.(ipcMessage, void 0, void 0, done);
|
|
4101
|
+
} : void 0,
|
|
4102
|
+
onMessage: (listener) => process.on("message", listener),
|
|
4103
|
+
onDisconnect: (listener) => process.on("disconnect", listener)
|
|
4104
|
+
});
|
|
4105
|
+
const refreshApiKey = apiKeyRefreshEnabled ? createIpcApiKeyRefresher(ipcCall, {
|
|
4106
|
+
providerName: agent.model.split(":", 1)[0] ?? "",
|
|
4107
|
+
agentId: agent.agent.id,
|
|
4108
|
+
turnId
|
|
4109
|
+
}) : void 0;
|
|
2599
4110
|
const messagingHost = createIpcMessagingHost(ipcCall);
|
|
2600
4111
|
const userMdHost = createIpcUserMdHost(ipcCall);
|
|
2601
4112
|
const browserHost = browserEnabled ? createIpcBrowserHost(ipcCall) : void 0;
|
|
2602
4113
|
const mcpHost = mcpTools && mcpTools.length > 0 ? createIpcMcpHost(ipcCall) : void 0;
|
|
4114
|
+
const bashApprovalHost = createIpcBashApprovalHost(ipcCall, {
|
|
4115
|
+
agent,
|
|
4116
|
+
turnId,
|
|
4117
|
+
bashApprovalMode
|
|
4118
|
+
});
|
|
4119
|
+
const reviewSessionDir = mode === "review" ? mkdtempSync3(join19(paths.logsDir, "review-session-")) : null;
|
|
4120
|
+
const proposals = [];
|
|
4121
|
+
let proposalBatchSubmitted = false;
|
|
4122
|
+
const allowedEvidence = new Set(
|
|
4123
|
+
(review?.evidence ?? []).map((item) => `${item.sessionId}:${item.entryOrdinal}`)
|
|
4124
|
+
);
|
|
4125
|
+
const reviewTools = [
|
|
4126
|
+
{
|
|
4127
|
+
name: "propose_lesson",
|
|
4128
|
+
label: "propose_lesson",
|
|
4129
|
+
description: "Submit zero to five evidence-backed lesson proposals in one final batch. This is the only available action.",
|
|
4130
|
+
parameters: Type3.Object({
|
|
4131
|
+
proposals: Type3.Array(
|
|
4132
|
+
Type3.Object({
|
|
4133
|
+
scope: Type3.Union([Type3.Literal("private"), Type3.Literal("shared")]),
|
|
4134
|
+
text: Type3.String({ minLength: 1, maxLength: 500 }),
|
|
4135
|
+
evidenceEntryIds: Type3.Array(
|
|
4136
|
+
Type3.Object({ sessionId: Type3.String(), entryOrdinal: Type3.Integer({ minimum: 0 }) }),
|
|
4137
|
+
{ minItems: 1 }
|
|
4138
|
+
)
|
|
4139
|
+
}),
|
|
4140
|
+
{ maxItems: 5 }
|
|
4141
|
+
)
|
|
4142
|
+
}),
|
|
4143
|
+
async execute(_toolCallId, params) {
|
|
4144
|
+
if (proposalBatchSubmitted) throw new Error("propose_lesson may be called only once");
|
|
4145
|
+
proposalBatchSubmitted = true;
|
|
4146
|
+
const input = params;
|
|
4147
|
+
for (const candidate of input.proposals) {
|
|
4148
|
+
const text = candidate.text.trim();
|
|
4149
|
+
if (!text || text.length > 500) throw new Error("proposal text must be 1-500 characters");
|
|
4150
|
+
for (const evidence of candidate.evidenceEntryIds) {
|
|
4151
|
+
if (!allowedEvidence.has(`${evidence.sessionId}:${evidence.entryOrdinal}`)) {
|
|
4152
|
+
throw new Error("proposal cites evidence outside the supplied digest");
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
proposals.push({ ...candidate, text });
|
|
4156
|
+
}
|
|
4157
|
+
return {
|
|
4158
|
+
content: [{ type: "text", text: `accepted ${proposals.length} proposals` }],
|
|
4159
|
+
details: {},
|
|
4160
|
+
terminate: true
|
|
4161
|
+
};
|
|
4162
|
+
}
|
|
4163
|
+
}
|
|
4164
|
+
];
|
|
2603
4165
|
const { session, dispose } = await createBazilionSession({
|
|
2604
4166
|
agent,
|
|
2605
4167
|
paths,
|
|
@@ -2609,16 +4171,31 @@ async function main() {
|
|
|
2609
4171
|
messagingHost,
|
|
2610
4172
|
userMdHost,
|
|
2611
4173
|
apiKey,
|
|
4174
|
+
refreshApiKey,
|
|
2612
4175
|
browserHost,
|
|
2613
4176
|
mcpHost,
|
|
2614
4177
|
mcpTools,
|
|
4178
|
+
bashApprovalHost,
|
|
2615
4179
|
// deliver_file emits a `file` event straight onto our stdout frame stream.
|
|
2616
|
-
fileSink: (f) => emit({ kind: "event", event: { type: "file", ...f } })
|
|
4180
|
+
fileSink: (f) => emit({ kind: "event", event: { type: "file", ...f } }),
|
|
4181
|
+
...mode === "review" && reviewSessionDir ? {
|
|
4182
|
+
restricted: {
|
|
4183
|
+
systemPrompt: REVIEW_SYSTEM_PROMPT,
|
|
4184
|
+
tools: reviewTools,
|
|
4185
|
+
sessionDir: reviewSessionDir,
|
|
4186
|
+
reasoningLevel: agent.agent.reviewReasoningLevel
|
|
4187
|
+
}
|
|
4188
|
+
} : {}
|
|
2617
4189
|
});
|
|
2618
4190
|
abortSession = () => {
|
|
2619
4191
|
void session.abort();
|
|
2620
4192
|
};
|
|
4193
|
+
let reviewValidationFailures = 0;
|
|
2621
4194
|
const unsubscribe = session.subscribe((piEvent) => {
|
|
4195
|
+
if (mode === "review" && piEvent.type === "tool_execution_end" && piEvent.toolName === "propose_lesson" && piEvent.isError) {
|
|
4196
|
+
reviewValidationFailures += 1;
|
|
4197
|
+
if (reviewValidationFailures >= 2) void session.abort();
|
|
4198
|
+
}
|
|
2622
4199
|
for (const ev of translatePiEvent(piEvent)) {
|
|
2623
4200
|
emit({ kind: "event", event: ev });
|
|
2624
4201
|
}
|
|
@@ -2631,10 +4208,19 @@ async function main() {
|
|
|
2631
4208
|
}));
|
|
2632
4209
|
await session.prompt(message, promptImages.length > 0 ? { images: promptImages } : void 0);
|
|
2633
4210
|
await session.agent.waitForIdle();
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
4211
|
+
if (mode === "review" && reviewValidationFailures >= 2) {
|
|
4212
|
+
throw new Error("review output remained invalid after one schema-repair attempt");
|
|
4213
|
+
}
|
|
4214
|
+
if (mode === "review" && !proposalBatchSubmitted) {
|
|
4215
|
+
throw new Error("review output did not submit a proposal batch");
|
|
4216
|
+
}
|
|
4217
|
+
if (mode === "review") emit({ kind: "review_result", proposals });
|
|
4218
|
+
else {
|
|
4219
|
+
emit({
|
|
4220
|
+
kind: "done",
|
|
4221
|
+
messages: piMessagesToProviderView(session.agent.state.messages)
|
|
4222
|
+
});
|
|
4223
|
+
}
|
|
2638
4224
|
} catch (err) {
|
|
2639
4225
|
if (!aborted) {
|
|
2640
4226
|
emit({ kind: "event", event: { type: "error", error: err.message } });
|
|
@@ -2646,6 +4232,7 @@ async function main() {
|
|
|
2646
4232
|
process.off("SIGTERM", onSignal);
|
|
2647
4233
|
process.off("SIGINT", onSignal);
|
|
2648
4234
|
dispose();
|
|
4235
|
+
if (reviewSessionDir) rmSync12(reviewSessionDir, { recursive: true, force: true });
|
|
2649
4236
|
try {
|
|
2650
4237
|
process.disconnect?.();
|
|
2651
4238
|
} catch {
|
|
@@ -2659,4 +4246,21 @@ main().catch((err) => {
|
|
|
2659
4246
|
}
|
|
2660
4247
|
process.exit(1);
|
|
2661
4248
|
});
|
|
4249
|
+
var REVIEW_SYSTEM_PROMPT = `You are a restricted learning reviewer. Analyze only the supplied
|
|
4250
|
+
transcript digest and approved-lesson index. You cannot act on the world. Call propose_lesson once
|
|
4251
|
+
with zero to five proposals, then stop.
|
|
4252
|
+
|
|
4253
|
+
If propose_lesson rejects malformed output, correct the schema exactly once. If that repair is also
|
|
4254
|
+
rejected, stop; the review will fail.
|
|
4255
|
+
|
|
4256
|
+
Classify before proposing:
|
|
4257
|
+
1. Stable human preference or biography belongs to USER.md: do not propose it.
|
|
4258
|
+
2. Agent-specific verified behavior or strategy is private.
|
|
4259
|
+
3. Reusable verified Team project knowledge, decisions, or procedures are shared.
|
|
4260
|
+
4. Everything else produces no proposal.
|
|
4261
|
+
|
|
4262
|
+
Zero proposals is a successful result. Never save transient or environment-dependent failures,
|
|
4263
|
+
unresolved attempts, guesses, contradicted claims, one-off narratives, status updates, secrets,
|
|
4264
|
+
credentials, sensitive personal data, raw logs, copied text, or duplicates. Each proposal must cite
|
|
4265
|
+
at least one supplied evidence entry and one observation may appear in only one proposal.`;
|
|
2662
4266
|
//# sourceMappingURL=worker.js.map
|