@fungi.computer/truffle 0.2.2 → 0.3.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/private-files-patch.js +3 -17
- package/dist/private-invocation-computer.d.ts +224 -12
- package/dist/private-invocation-computer.js +162 -22
- package/dist/private-invocation-files.d.ts +3 -2
- package/dist/private-invocation-files.js +22 -3
- package/dist/private-local-agent-control.d.ts +2 -2
- package/dist/private-local-bwrap.d.ts +9 -7
- package/dist/private-local-bwrap.js +26 -21
- package/dist/private-local-computer-processes.d.ts +15 -0
- package/dist/private-local-computer-processes.js +13 -0
- package/dist/private-local-containment.d.ts +15 -13
- package/dist/private-local-containment.js +49 -65
- package/dist/private-local-delegation.d.ts +3 -3
- package/dist/private-local-delegation.js +32 -19
- package/dist/private-local-exec-review.d.ts +10 -0
- package/dist/private-local-exec-review.js +50 -0
- package/dist/private-local-exec-rules.d.ts +14 -0
- package/dist/private-local-exec-rules.js +420 -0
- package/dist/private-local-git-exclude.js +25 -15
- package/dist/private-local-linux-sandbox.d.ts +5 -5
- package/dist/private-local-linux-sandbox.js +63 -30
- package/dist/private-local-mail.d.ts +2 -2
- package/dist/private-local-mesh-links.d.ts +4 -1
- package/dist/private-local-mesh-links.js +50 -3
- package/dist/private-local-native-digests.js +1 -1
- package/dist/private-local-native-install-error.d.ts +2 -0
- package/dist/private-local-native-install-error.js +10 -0
- package/dist/private-local-native-staging.d.ts +7 -8
- package/dist/private-local-native-staging.js +132 -43
- package/dist/private-local-permission-mode.d.ts +10 -0
- package/dist/private-local-permission-mode.js +18 -0
- package/dist/private-local-runtime-host.d.ts +19 -5
- package/dist/private-local-runtime-host.js +378 -65
- package/dist/private-local-sandbox-policy.d.ts +30 -11
- package/dist/private-local-sandbox-policy.js +223 -373
- package/dist/private-local-sandbox.d.ts +30 -25
- package/dist/private-local-sandbox.js +190 -100
- package/dist/private-local-seatbelt.d.ts +7 -13
- package/dist/private-local-seatbelt.js +55 -32
- package/dist/private-local-service-contract.d.ts +5 -4
- package/dist/private-local-service-discovery.d.ts +19 -7
- package/dist/private-local-service-discovery.js +4 -3
- package/dist/private-local-service-listener.js +40 -2
- package/dist/private-local-service-log.d.ts +8 -0
- package/dist/private-local-service-log.js +3 -0
- package/dist/private-local-service-owner.d.ts +18 -10
- package/dist/private-local-service-owner.js +117 -25
- package/dist/private-local-service.js +30 -3
- package/dist/private-local-session-discovery.d.ts +2 -1
- package/dist/private-local-session-discovery.js +18 -6
- package/dist/private-mcp-form-contract.d.ts +8 -1
- package/dist/private-mcp-form-contract.js +8 -1
- package/dist/private-mcp-form-session.d.ts +33 -3
- package/dist/private-mcp-form-session.js +28 -5
- package/dist/private-oyster-mcp-auth.d.ts +1 -1
- package/dist/private-oyster-mcp-auth.js +139 -7
- package/dist/private-provider-rejection.js +1 -1
- package/dist/seatbelt/safe-base.sbpl +2 -2
- package/native/bin/bwrap-build-source/Dockerfile +2 -0
- package/native/bin/bwrap-build-source/README.md +6 -6
- package/native/bin/bwrap-build-source/pin.mjs +2 -2
- package/native/bin/bwrap-build-source/record.mjs +12 -1
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +10 -7
- package/native/bin/bwrap-build.json +3 -2
- package/native/bin/fungi-apply-patch +0 -0
- package/native/bin/fungi-seccomp-workspace.bpf +0 -0
- package/native/bin/fungi-seccomp-workspace.bpf.sha256 +1 -0
- package/package.json +23 -17
|
@@ -5,15 +5,18 @@ import { modelFailureMessage, modelFailureRetryable, } from "./private-provider-
|
|
|
5
5
|
import { logEvent } from "./log-event.js";
|
|
6
6
|
import { localArchiveHandoff } from "./local-archive-handoff.js";
|
|
7
7
|
import { createMcpFormSession } from "./private-mcp-form-session.js";
|
|
8
|
+
import { localExecEscapeReview } from "./private-local-exec-review.js";
|
|
9
|
+
import { ApprovalLedger, } from "@fungi.computer/approvals";
|
|
8
10
|
import { localExtensions, } from "./local-extensions.js";
|
|
9
11
|
import { lstat, mkdir, realpath, stat } from "node:fs/promises";
|
|
10
12
|
import { lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
11
13
|
import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
|
|
12
14
|
import { createHash } from "node:crypto";
|
|
13
15
|
import { z } from "zod";
|
|
16
|
+
import { defaultLocalPermissionMode, localPermissionModeSchema, modeWithinCeiling, } from "./private-local-permission-mode.js";
|
|
14
17
|
import { cleanupSessionResources, } from "@earendil-works/pi-ai";
|
|
15
18
|
import { parseModelPreference, } from "@fungi.computer/shiitake/model-preference";
|
|
16
|
-
import { createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
|
|
19
|
+
import { AgentCreateRefused, createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
|
|
17
20
|
import { defineIdFamily } from "@fungi.computer/cairn";
|
|
18
21
|
import { words } from "@fungi.computer/cairn/words";
|
|
19
22
|
import { botanicalExtensionsWithSkills } from "@fungi.computer/shiitake-extensions/roster";
|
|
@@ -28,12 +31,14 @@ import { ensureAgentDefaultModelTable, readAgentDefaultModel, seedAgentDefaultMo
|
|
|
28
31
|
import { defaultLocalReasoning, resolveKnownLocalStartupChoice, resolveLocalStartupChoice, startupReasoning, validateLocalReasoning, } from "./private-local-startup-model.js";
|
|
29
32
|
import { assertFungiModelsEnabled } from "./private-fungi-model-api.js";
|
|
30
33
|
import { privateLocalEffectiveModelSchema, } from "./private-local-effective-model.js";
|
|
31
|
-
import { createPrivateInvocationComputer, } from "./private-invocation-computer.js";
|
|
34
|
+
import { createPrivateInvocationComputer, dispatchApprovedLocalExec, shellOutput, } from "./private-invocation-computer.js";
|
|
35
|
+
import { localExecRules } from "./private-local-exec-rules.js";
|
|
32
36
|
import { createPrivateLocalComputerProcesses, } from "./private-local-computer-processes.js";
|
|
33
37
|
import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
|
|
34
38
|
import { ensurePrivateLocalGitExclude } from "./private-local-git-exclude.js";
|
|
35
|
-
import { createLocalSandboxControls, } from "./private-local-sandbox.js";
|
|
36
|
-
import {
|
|
39
|
+
import { createLocalSandboxControls, probeLocalSandbox, } from "./private-local-sandbox.js";
|
|
40
|
+
import { LocalSandboxUnavailable } from "./private-local-containment.js";
|
|
41
|
+
import { gitDirectories, protectedGitRemoteNames, localAgentFolderSchema, resolveAgentFolder, } from "./private-local-sandbox-policy.js";
|
|
37
42
|
import { LocalStoreSchemaError } from "./private-local-session-discovery.js";
|
|
38
43
|
import { acquireNodeStoreLease, nodeRuntimeHost, } from "@fungi.computer/shiitake/node";
|
|
39
44
|
import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js";
|
|
@@ -65,7 +70,8 @@ function isErrnoException(error) {
|
|
|
65
70
|
function sidecarPathFor(databasePath) {
|
|
66
71
|
return `${databasePath}.owner.sqlite`;
|
|
67
72
|
}
|
|
68
|
-
function ensureLocalAgentId(
|
|
73
|
+
function ensureLocalAgentId(options) {
|
|
74
|
+
const { database, databasePath, project, pathAgentId, name, createdBy, folder, mode, native, } = options;
|
|
69
75
|
database.exec("BEGIN IMMEDIATE");
|
|
70
76
|
try {
|
|
71
77
|
database.exec(`
|
|
@@ -76,16 +82,24 @@ function ensureLocalAgentId(database, databasePath, pathAgentId, name, createdBy
|
|
|
76
82
|
created_by TEXT,
|
|
77
83
|
archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1)),
|
|
78
84
|
folder TEXT NOT NULL DEFAULT '.',
|
|
79
|
-
|
|
85
|
+
mode TEXT NOT NULL CHECK (mode IN ('full-access', 'workspace', 'read-only')),
|
|
86
|
+
writable_folders TEXT NOT NULL DEFAULT '[]'
|
|
80
87
|
)
|
|
81
88
|
`);
|
|
82
89
|
const columns = database
|
|
83
90
|
.prepare("PRAGMA table_info(private_local_identity)")
|
|
84
91
|
.all();
|
|
85
|
-
if (![
|
|
92
|
+
if (![
|
|
93
|
+
"name",
|
|
94
|
+
"created_by",
|
|
95
|
+
"archived",
|
|
96
|
+
"folder",
|
|
97
|
+
"mode",
|
|
98
|
+
"writable_folders",
|
|
99
|
+
].every((required) => columns.some((column) => column.name === required)))
|
|
86
100
|
throw new LocalStoreSchemaError(databasePath);
|
|
87
101
|
const row = database
|
|
88
|
-
.prepare("SELECT agent_id, created_by, folder FROM private_local_identity WHERE singleton = 1")
|
|
102
|
+
.prepare("SELECT agent_id, created_by, folder, mode FROM private_local_identity WHERE singleton = 1")
|
|
89
103
|
.get();
|
|
90
104
|
if (row !== undefined) {
|
|
91
105
|
const parsed = localHostIds.store.schema.safeParse(row.agent_id);
|
|
@@ -98,18 +112,37 @@ function ensureLocalAgentId(database, databasePath, pathAgentId, name, createdBy
|
|
|
98
112
|
if (folder !== undefined &&
|
|
99
113
|
localAgentFolderSchema.parse(folder) !== storedFolder)
|
|
100
114
|
throw new Error("Local Agent folder conflicts with its store");
|
|
101
|
-
if (
|
|
115
|
+
if (mode !== undefined) {
|
|
116
|
+
if (createdBy !== undefined) {
|
|
117
|
+
if (!modeWithinCeiling(localPermissionModeSchema.parse(row.mode), mode))
|
|
118
|
+
throw new AgentCreateRefused("exceeds_creator", "Child Agent mode exceeds its creator's mode");
|
|
119
|
+
database.exec("COMMIT");
|
|
120
|
+
return parsed.data;
|
|
121
|
+
}
|
|
122
|
+
if (mode !== "full-access") {
|
|
123
|
+
const reason = probeLocalSandbox(project, storedFolder, native);
|
|
124
|
+
if (reason !== undefined)
|
|
125
|
+
throw new LocalSandboxUnavailable(reason);
|
|
126
|
+
}
|
|
102
127
|
database
|
|
103
|
-
.prepare("UPDATE private_local_identity SET
|
|
104
|
-
.run(
|
|
128
|
+
.prepare("UPDATE private_local_identity SET mode = ? WHERE singleton = 1")
|
|
129
|
+
.run(mode);
|
|
130
|
+
}
|
|
105
131
|
database.exec("COMMIT");
|
|
106
132
|
return parsed.data;
|
|
107
133
|
}
|
|
108
134
|
const agentId = pathAgentId ?? localHostIds.store.generate();
|
|
135
|
+
const selectedFolder = localAgentFolderSchema.parse(folder ?? ".");
|
|
136
|
+
const unavailable = mode === "full-access"
|
|
137
|
+
? undefined
|
|
138
|
+
: probeLocalSandbox(project, selectedFolder, native);
|
|
139
|
+
if (mode !== undefined && unavailable !== undefined)
|
|
140
|
+
throw new LocalSandboxUnavailable(unavailable);
|
|
141
|
+
const selectedMode = mode ?? defaultLocalPermissionMode(unavailable === undefined);
|
|
109
142
|
database
|
|
110
|
-
.prepare("INSERT INTO private_local_identity (singleton, agent_id, name, created_by, folder,
|
|
143
|
+
.prepare("INSERT INTO private_local_identity (singleton, agent_id, name, created_by, folder, mode, writable_folders) VALUES (1, ?, ?, ?, ?, ?, '[]')")
|
|
111
144
|
// An unnamed Agent gets a Cairn candidate word name so it never shows its id.
|
|
112
|
-
.run(agentId, name ?? words(), createdBy ?? null,
|
|
145
|
+
.run(agentId, name ?? words(), createdBy ?? null, selectedFolder, selectedMode);
|
|
113
146
|
database.exec("COMMIT");
|
|
114
147
|
return agentId;
|
|
115
148
|
}
|
|
@@ -563,38 +596,66 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
563
596
|
modelFailure?.message === message &&
|
|
564
597
|
modelFailure.retryable,
|
|
565
598
|
failureReason: (message) => modelFailure?.message === message ? modelFailure.reason : undefined,
|
|
599
|
+
providerError: (message) => modelFailure?.message === message
|
|
600
|
+
? modelFailure.providerError
|
|
601
|
+
: undefined,
|
|
566
602
|
stream: (selected, context, options) => {
|
|
603
|
+
const startedAt = Date.now();
|
|
567
604
|
modelFailure = undefined;
|
|
605
|
+
let lastPartial;
|
|
568
606
|
const streamOptions = { ...options };
|
|
569
607
|
const reasoning = options?.reasoning ?? selectedReasoning;
|
|
570
608
|
if (reasoning !== undefined)
|
|
571
609
|
streamOptions.reasoning = reasoning;
|
|
572
610
|
const provider = observeProviderStatus(streamOptions);
|
|
573
611
|
const events = createAssistantMessageEventStream();
|
|
574
|
-
const failure = (
|
|
612
|
+
const failure = (aborted, upstream, cause) => {
|
|
613
|
+
const partial = upstream?.content.some((part) => part.type !== "text" || part.text.length > 0)
|
|
614
|
+
? upstream
|
|
615
|
+
: (lastPartial ?? upstream);
|
|
575
616
|
const evidence = {
|
|
576
617
|
refusal: turn.refusal(),
|
|
577
|
-
upstream,
|
|
618
|
+
upstream: upstream ?? partial,
|
|
578
619
|
rejection: provider.rejection(),
|
|
579
620
|
};
|
|
580
|
-
const message =
|
|
621
|
+
const message = cause === undefined
|
|
622
|
+
? modelFailureMessage(selected.provider, evidence)
|
|
623
|
+
: `${selected.provider} ${cause instanceof Error && cause.name === "TimeoutError" ? "request timed out" : cause instanceof Error && cause.name === "AbortError" ? "request was aborted" : "connection failed"} after ${String(Date.now() - startedAt)} ms. Retry the request.`;
|
|
581
624
|
const result = {
|
|
582
625
|
role: "assistant",
|
|
583
|
-
content: [],
|
|
626
|
+
content: partial?.content ?? [],
|
|
584
627
|
api: selected.api,
|
|
585
628
|
provider: selected.provider,
|
|
586
629
|
model: selected.id,
|
|
587
|
-
usage
|
|
630
|
+
usage: partial?.usage ?? {
|
|
631
|
+
input: 0,
|
|
632
|
+
output: 0,
|
|
633
|
+
cacheRead: 0,
|
|
634
|
+
cacheWrite: 0,
|
|
635
|
+
totalTokens: 0,
|
|
636
|
+
cost: {
|
|
637
|
+
input: 0,
|
|
638
|
+
output: 0,
|
|
639
|
+
cacheRead: 0,
|
|
640
|
+
cacheWrite: 0,
|
|
641
|
+
total: 0,
|
|
642
|
+
},
|
|
643
|
+
},
|
|
588
644
|
stopReason: aborted ? "aborted" : "error",
|
|
589
645
|
timestamp: Date.now(),
|
|
590
|
-
errorMessage: aborted ? "Run cancelled" : message,
|
|
591
646
|
};
|
|
592
|
-
|
|
647
|
+
// The summary caller and retries receive only the safe reason.
|
|
648
|
+
// The turn recorder obtains provider prose by exact message identity.
|
|
649
|
+
result.errorMessage = aborted ? "Run cancelled" : message;
|
|
650
|
+
if (!aborted) {
|
|
593
651
|
modelFailure = {
|
|
594
652
|
message: result,
|
|
595
653
|
reason: message,
|
|
596
654
|
retryable: modelFailureRetryable(evidence),
|
|
597
655
|
};
|
|
656
|
+
if (upstream?.errorMessage !== undefined)
|
|
657
|
+
modelFailure.providerError = upstream.errorMessage;
|
|
658
|
+
}
|
|
598
659
|
return result;
|
|
599
660
|
};
|
|
600
661
|
void (async () => {
|
|
@@ -613,32 +674,38 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
613
674
|
requestOptions.onResponse = (response, model) => onResponse(response, normalizeModel(model));
|
|
614
675
|
const upstream = turn.models.streamSimple(selected, normalizeContext(context), requestOptions);
|
|
615
676
|
for await (const event of upstream) {
|
|
677
|
+
if ("partial" in event)
|
|
678
|
+
lastPartial = event.partial;
|
|
616
679
|
if (event.type === "error") {
|
|
617
|
-
const error = failure(event.
|
|
680
|
+
const error = failure(event.reason === "aborted", event.error);
|
|
618
681
|
events.push({ ...event, error });
|
|
619
682
|
events.end(error);
|
|
620
683
|
return;
|
|
621
684
|
}
|
|
685
|
+
if (event.type === "done" &&
|
|
686
|
+
(event.message.stopReason === "error" ||
|
|
687
|
+
event.message.stopReason === "aborted")) {
|
|
688
|
+
const aborted = event.message.stopReason === "aborted";
|
|
689
|
+
const error = failure(aborted, event.message);
|
|
690
|
+
events.push({
|
|
691
|
+
type: "error",
|
|
692
|
+
reason: aborted ? "aborted" : "error",
|
|
693
|
+
error,
|
|
694
|
+
});
|
|
695
|
+
events.end(error);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
622
698
|
events.push(event);
|
|
623
699
|
}
|
|
624
|
-
|
|
700
|
+
const result = await upstream.result();
|
|
701
|
+
events.end(result.stopReason === "error" ||
|
|
702
|
+
result.stopReason === "aborted"
|
|
703
|
+
? failure(result.stopReason === "aborted", result)
|
|
704
|
+
: result);
|
|
625
705
|
}
|
|
626
|
-
catch {
|
|
706
|
+
catch (cause) {
|
|
627
707
|
const aborted = (options?.signal ?? signal).aborted;
|
|
628
|
-
const error = failure(
|
|
629
|
-
input: 0,
|
|
630
|
-
output: 0,
|
|
631
|
-
cacheRead: 0,
|
|
632
|
-
cacheWrite: 0,
|
|
633
|
-
totalTokens: 0,
|
|
634
|
-
cost: {
|
|
635
|
-
input: 0,
|
|
636
|
-
output: 0,
|
|
637
|
-
cacheRead: 0,
|
|
638
|
-
cacheWrite: 0,
|
|
639
|
-
total: 0,
|
|
640
|
-
},
|
|
641
|
-
}, aborted);
|
|
708
|
+
const error = failure(aborted, undefined, cause);
|
|
642
709
|
events.push({
|
|
643
710
|
type: "error",
|
|
644
711
|
reason: aborted ? "aborted" : "error",
|
|
@@ -655,7 +722,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
655
722
|
});
|
|
656
723
|
}
|
|
657
724
|
/** One local Computer source, resolving its PATH and process scope per Session. */
|
|
658
|
-
function createPrivateLocalComputerSource(root, port, sessionPath, processes, home, sandbox) {
|
|
725
|
+
function createPrivateLocalComputerSource(root, port, sessionPath, processes, home, sandbox, mode = () => "full-access", escapeForSession, policyForSession) {
|
|
659
726
|
const id = "local-computer";
|
|
660
727
|
return {
|
|
661
728
|
id,
|
|
@@ -665,7 +732,7 @@ function createPrivateLocalComputerSource(root, port, sessionPath, processes, ho
|
|
|
665
732
|
throw new Error("Local Computer requires a Session");
|
|
666
733
|
const path = sessionPath(sessionId) ??
|
|
667
734
|
capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd());
|
|
668
|
-
const selected = await createPrivateInvocationComputer(root, path, processes.scope(sessionId), home, sandbox);
|
|
735
|
+
const selected = await createPrivateInvocationComputer(root, path, processes.scope(sessionId), home, sandbox, mode, escapeForSession?.(sessionId), policyForSession?.(sessionId));
|
|
669
736
|
return Knapsack.computer({
|
|
670
737
|
id,
|
|
671
738
|
name: "local",
|
|
@@ -690,10 +757,10 @@ storeDirectory,
|
|
|
690
757
|
* This Agent's sub-agent record: its Session-bound Agent-control tools and
|
|
691
758
|
* the view's facts. Present when it can direct other Agents.
|
|
692
759
|
*/
|
|
693
|
-
subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox) {
|
|
760
|
+
subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox, currentMode = () => "full-access", escapeForSession, policyForSession) {
|
|
694
761
|
const forms = createMcpFormSession();
|
|
695
762
|
const root = await realpath(resolve(invocationRoot));
|
|
696
|
-
const port = await createPrivateInvocationFilesPort(root, undefined, sandbox);
|
|
763
|
+
const port = await createPrivateInvocationFilesPort(root, undefined, sandbox, currentMode);
|
|
697
764
|
const processes = createPrivateLocalComputerProcesses(root, configRoot.home);
|
|
698
765
|
const computerSources = sessionPath === undefined
|
|
699
766
|
? [
|
|
@@ -705,11 +772,11 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox)
|
|
|
705
772
|
port,
|
|
706
773
|
pathInstructions: localPathInstructions(root),
|
|
707
774
|
},
|
|
708
|
-
computer: await createPrivateInvocationComputer(root, undefined, processes.scope("invocation"), configRoot.home, sandbox?.launch),
|
|
775
|
+
computer: await createPrivateInvocationComputer(root, undefined, processes.scope("invocation"), configRoot.home, sandbox?.launch, currentMode, escapeForSession?.("invocation"), policyForSession?.("invocation")),
|
|
709
776
|
}),
|
|
710
777
|
]
|
|
711
778
|
: [
|
|
712
|
-
createPrivateLocalComputerSource(root, port, sessionPath, processes, configRoot.home, sandbox?.launch),
|
|
779
|
+
createPrivateLocalComputerSource(root, port, sessionPath, processes, configRoot.home, sandbox?.launch, currentMode, escapeForSession, policyForSession),
|
|
713
780
|
];
|
|
714
781
|
const installed = configuration?.installed ??
|
|
715
782
|
(storeDirectory === undefined
|
|
@@ -733,7 +800,11 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox)
|
|
|
733
800
|
homeExtension({
|
|
734
801
|
computers: {
|
|
735
802
|
read: () => [
|
|
736
|
-
{
|
|
803
|
+
{
|
|
804
|
+
name: "local",
|
|
805
|
+
attachmentId: "local-computer",
|
|
806
|
+
port,
|
|
807
|
+
},
|
|
737
808
|
],
|
|
738
809
|
},
|
|
739
810
|
}),
|
|
@@ -766,7 +837,7 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox)
|
|
|
766
837
|
...(clientPath === undefined
|
|
767
838
|
? computerSources
|
|
768
839
|
: [
|
|
769
|
-
createPrivateLocalComputerSource(root, port, () => clientPath, processes, configRoot.home, sandbox?.launch),
|
|
840
|
+
createPrivateLocalComputerSource(root, port, () => clientPath, processes, configRoot.home, sandbox?.launch, currentMode, escapeForSession, policyForSession),
|
|
770
841
|
]),
|
|
771
842
|
...(connection === undefined
|
|
772
843
|
? []
|
|
@@ -804,9 +875,13 @@ function localRuntimeOwner() {
|
|
|
804
875
|
}),
|
|
805
876
|
settled: async () => {
|
|
806
877
|
if (pendingWork === undefined)
|
|
807
|
-
return false;
|
|
878
|
+
return { idle: false, wakeAt: null };
|
|
808
879
|
const work = await pendingWork();
|
|
809
|
-
return
|
|
880
|
+
return {
|
|
881
|
+
idle: !work.pending &&
|
|
882
|
+
(work.nextWakeAt === null || work.nextWakeAt > Date.now()),
|
|
883
|
+
wakeAt: work.nextWakeAt,
|
|
884
|
+
};
|
|
810
885
|
},
|
|
811
886
|
});
|
|
812
887
|
}
|
|
@@ -917,6 +992,16 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
917
992
|
throw new ShiitakeError("model", "rejected", `Local model is not registered: ${selectedProviderId}/${selectedModelId ?? "select with --model"}`);
|
|
918
993
|
validateLocalReasoning(model, input.reasoning);
|
|
919
994
|
}
|
|
995
|
+
// A new explicit sandboxed Agent must be admissible before resolving its
|
|
996
|
+
// create receipt, directory, sidecar or SQLite file. Keep the check inside
|
|
997
|
+
// ensureLocalAgentId too: policy may change between this probe and commit.
|
|
998
|
+
if (input.databasePath === undefined &&
|
|
999
|
+
input.mode !== undefined &&
|
|
1000
|
+
input.mode !== "full-access") {
|
|
1001
|
+
const reason = probeLocalSandbox(root, localAgentFolderSchema.parse(input.folder ?? "."), input.native);
|
|
1002
|
+
if (reason !== undefined)
|
|
1003
|
+
throw new LocalSandboxUnavailable(reason);
|
|
1004
|
+
}
|
|
920
1005
|
const paths = await localDatabasePaths(root, input.databasePath, input.createRequestId);
|
|
921
1006
|
const runtime = localRuntimeOwner();
|
|
922
1007
|
let lease;
|
|
@@ -935,20 +1020,34 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
935
1020
|
.digest("hex");
|
|
936
1021
|
database = openPrivateWritableSqliteDatabase(paths.databasePath, 0);
|
|
937
1022
|
database.exec("PRAGMA foreign_keys = ON");
|
|
938
|
-
const agentId = ensureLocalAgentId(
|
|
1023
|
+
const agentId = ensureLocalAgentId({
|
|
1024
|
+
database,
|
|
1025
|
+
databasePath: paths.databasePath,
|
|
1026
|
+
project: root,
|
|
1027
|
+
pathAgentId: paths.agentId,
|
|
1028
|
+
name: input.name,
|
|
1029
|
+
createdBy: input.createdBy,
|
|
1030
|
+
folder: input.folder,
|
|
1031
|
+
mode: input.mode,
|
|
1032
|
+
native: input.native,
|
|
1033
|
+
});
|
|
939
1034
|
const sandboxDatabase = database;
|
|
940
|
-
const
|
|
941
|
-
.
|
|
942
|
-
.
|
|
943
|
-
.prepare("SELECT require_sandbox FROM private_local_identity WHERE singleton = 1")
|
|
944
|
-
.get()?.require_sandbox) === 1;
|
|
1035
|
+
const readMode = () => localPermissionModeSchema.parse(sandboxDatabase
|
|
1036
|
+
.prepare("SELECT mode FROM private_local_identity WHERE singleton = 1")
|
|
1037
|
+
.get()?.mode);
|
|
945
1038
|
const selectedFolder = resolveAgentFolder(root, localAgentFolderSchema.parse(database
|
|
946
1039
|
.prepare("SELECT folder FROM private_local_identity WHERE singleton = 1")
|
|
947
1040
|
.get()?.folder));
|
|
1041
|
+
const defaultReason = readMode() === "full-access" && input.mode === undefined
|
|
1042
|
+
? probeLocalSandbox(root, selectedFolder.folder, input.native)
|
|
1043
|
+
: undefined;
|
|
948
1044
|
const sandboxOwner = createLocalSandboxControls({
|
|
949
1045
|
project: root,
|
|
950
1046
|
folder: selectedFolder.folder,
|
|
951
|
-
|
|
1047
|
+
agentId,
|
|
1048
|
+
mode: readMode,
|
|
1049
|
+
defaultReason,
|
|
1050
|
+
native: input.native,
|
|
952
1051
|
});
|
|
953
1052
|
sandbox = sandboxOwner;
|
|
954
1053
|
// Both constructors below can resume stored commands before returning.
|
|
@@ -1015,7 +1114,178 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1015
1114
|
directory: projectDirectory,
|
|
1016
1115
|
storage: store,
|
|
1017
1116
|
});
|
|
1018
|
-
const
|
|
1117
|
+
const escapeLedger = new ApprovalLedger({
|
|
1118
|
+
owner: createPrivateSqliteOwner(database),
|
|
1119
|
+
home: { kind: "local", id: agentId },
|
|
1120
|
+
now: Date.now,
|
|
1121
|
+
});
|
|
1122
|
+
const execRules = localExecRules(root);
|
|
1123
|
+
// An Agent can create .git after opening the Session. That does not make
|
|
1124
|
+
// its newly authored config/hooks a protected repository for a grant.
|
|
1125
|
+
const grantRepository = lstatSync(join(root, ".git"), { throwIfNoEntry: false }) === undefined
|
|
1126
|
+
? undefined
|
|
1127
|
+
: gitDirectories(root, root);
|
|
1128
|
+
// The attached human's answer, never form JSON or the model, selects
|
|
1129
|
+
// this short-lived intent. Commit the rule before dispatching the command.
|
|
1130
|
+
const ruleIntents = new Map();
|
|
1131
|
+
const selectRule = (recordId, prefix) => (selected) => {
|
|
1132
|
+
if (selected === "prefix")
|
|
1133
|
+
ruleIntents.set(recordId, () => {
|
|
1134
|
+
execRules.add(prefix);
|
|
1135
|
+
});
|
|
1136
|
+
else
|
|
1137
|
+
ruleIntents.delete(recordId);
|
|
1138
|
+
};
|
|
1139
|
+
const escapeFence = () => JSON.stringify([
|
|
1140
|
+
readMode(),
|
|
1141
|
+
resolveAgentFolder(root, localAgentFolderSchema.parse(sandboxDatabase
|
|
1142
|
+
.prepare("SELECT folder FROM private_local_identity WHERE singleton = 1")
|
|
1143
|
+
.get()?.folder)).absolute,
|
|
1144
|
+
]);
|
|
1145
|
+
const escapeApply = {
|
|
1146
|
+
recheck: () => Promise.resolve({
|
|
1147
|
+
kind: "authorized",
|
|
1148
|
+
authorityFence: escapeFence(),
|
|
1149
|
+
}),
|
|
1150
|
+
dispatch: async ({ record, input: payload }) => {
|
|
1151
|
+
if (record.module !== "computer" || record.operation !== "exec")
|
|
1152
|
+
return { kind: "refused", reason: "not-granted" };
|
|
1153
|
+
const evidence = z
|
|
1154
|
+
.object({
|
|
1155
|
+
request: z.unknown(),
|
|
1156
|
+
cwd: z.string(),
|
|
1157
|
+
executionPath: z.string(),
|
|
1158
|
+
})
|
|
1159
|
+
.parse(payload);
|
|
1160
|
+
// Refuse a replaced or re-pointed working directory: the card showed
|
|
1161
|
+
// the canonical path selected at staging, not a later symlink target.
|
|
1162
|
+
if ((await realpath(evidence.cwd)) !== evidence.cwd)
|
|
1163
|
+
return { kind: "refused", reason: "not-granted" };
|
|
1164
|
+
const sessionId = record.requester.sessionId;
|
|
1165
|
+
if (sessionId === null)
|
|
1166
|
+
return { kind: "refused", reason: "not-granted" };
|
|
1167
|
+
const output = await dispatchApprovedLocalExec(evidence.request, evidence.cwd, evidence.executionPath, configRoot.home, localTools.processes.scope(sessionId), readMode());
|
|
1168
|
+
return { kind: "completed", output: z.json().parse(output) };
|
|
1169
|
+
},
|
|
1170
|
+
};
|
|
1171
|
+
const deliverEscapeResult = async (recordId) => {
|
|
1172
|
+
await escapeLedger.handoff(recordId, async (notice) => {
|
|
1173
|
+
const sessionId = notice.record.requester.sessionId;
|
|
1174
|
+
if (sessionId === null)
|
|
1175
|
+
throw new Error("Approval has no recipient Session");
|
|
1176
|
+
const accepted = await sessionOwner
|
|
1177
|
+
.session(sessionId)
|
|
1178
|
+
.send(`Approval ${notice.record.module}.${notice.record.operation}: ${JSON.stringify(notice.result)}`, { requestId: notice.deliveryId, delivery: "followUp" });
|
|
1179
|
+
return { commandId: accepted.commandId };
|
|
1180
|
+
});
|
|
1181
|
+
};
|
|
1182
|
+
const escapeApprovals = escapeLedger.capability({
|
|
1183
|
+
// Only the attached Local UI reaches the form's answer command. The
|
|
1184
|
+
// model has no member-bound approval capability.
|
|
1185
|
+
actor: () => Promise.resolve({
|
|
1186
|
+
kind: "human",
|
|
1187
|
+
memberId: agentId,
|
|
1188
|
+
role: "owner",
|
|
1189
|
+
actingForAgent: false,
|
|
1190
|
+
mayUseOperation: true,
|
|
1191
|
+
}),
|
|
1192
|
+
decision: async (recordId) => {
|
|
1193
|
+
const answered = escapeLedger.get(recordId);
|
|
1194
|
+
if (answered?.state === "approved") {
|
|
1195
|
+
if (answered.module === "computer" &&
|
|
1196
|
+
answered.operation === "exec" &&
|
|
1197
|
+
answered.authorityFence === escapeFence())
|
|
1198
|
+
ruleIntents.get(recordId)?.();
|
|
1199
|
+
await escapeLedger.apply(recordId, escapeApply);
|
|
1200
|
+
}
|
|
1201
|
+
for (const pending of escapeLedger.pendingHandoffs())
|
|
1202
|
+
await deliverEscapeResult(pending);
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1205
|
+
const escapeForSession = (sessionId) => async ({ request, cwd, mode, denialLine, context, executionPath }) => {
|
|
1206
|
+
if (request.escape === undefined)
|
|
1207
|
+
throw new Error("An escape requires a justification");
|
|
1208
|
+
// The identity, current mode and root are host facts, not model input.
|
|
1209
|
+
const staged = await escapeLedger.request({
|
|
1210
|
+
requester: {
|
|
1211
|
+
principalId: agentId,
|
|
1212
|
+
agentId,
|
|
1213
|
+
sessionId,
|
|
1214
|
+
displayName: name,
|
|
1215
|
+
},
|
|
1216
|
+
module: "computer",
|
|
1217
|
+
operation: "exec",
|
|
1218
|
+
access: "external",
|
|
1219
|
+
reversibility: "irreversible-external",
|
|
1220
|
+
authorityFence: JSON.stringify([mode, selectedFolder.absolute]),
|
|
1221
|
+
idempotencyKey: JSON.stringify([
|
|
1222
|
+
context.executionId,
|
|
1223
|
+
context.toolCallId ?? null,
|
|
1224
|
+
context.operationOrdinal ?? null,
|
|
1225
|
+
]),
|
|
1226
|
+
inputJson: JSON.stringify({
|
|
1227
|
+
request,
|
|
1228
|
+
cwd,
|
|
1229
|
+
mode,
|
|
1230
|
+
denialLine,
|
|
1231
|
+
executionPath,
|
|
1232
|
+
}),
|
|
1233
|
+
requireHumanApproval: true,
|
|
1234
|
+
});
|
|
1235
|
+
if (staged.kind === "refused")
|
|
1236
|
+
return staged;
|
|
1237
|
+
if (staged.record.state !== "pending")
|
|
1238
|
+
return {
|
|
1239
|
+
kind: "refused",
|
|
1240
|
+
reason: staged.record.state === "expired"
|
|
1241
|
+
? "expired"
|
|
1242
|
+
: "already-answered",
|
|
1243
|
+
};
|
|
1244
|
+
const pending = {
|
|
1245
|
+
kind: "pending_approval",
|
|
1246
|
+
recordId: staged.record.recordId,
|
|
1247
|
+
description: staged.record.description,
|
|
1248
|
+
};
|
|
1249
|
+
const review = localExecEscapeReview(request, cwd);
|
|
1250
|
+
const presented = await localTools.presentApproval(sessionId, pending, escapeApprovals, review, review.prefix === undefined
|
|
1251
|
+
? undefined
|
|
1252
|
+
: selectRule(pending.recordId, review.prefix));
|
|
1253
|
+
if (presented.kind !== "presented")
|
|
1254
|
+
throw new Error("Local escape approval could not be presented");
|
|
1255
|
+
return pending;
|
|
1256
|
+
};
|
|
1257
|
+
const policyForSession = (sessionId) => async ({ request, cwd, mode, executionPath }) => {
|
|
1258
|
+
if (mode !== "workspace" || !("executable" in request))
|
|
1259
|
+
return undefined;
|
|
1260
|
+
let configuredRemotes;
|
|
1261
|
+
if (request.executable === "git") {
|
|
1262
|
+
// Only the project's own protected common Git directory may use a
|
|
1263
|
+
// Git grant. A nested/Agent-created repository has writable config.
|
|
1264
|
+
const protectedGit = grantRepository === undefined
|
|
1265
|
+
? undefined
|
|
1266
|
+
: gitDirectories(root, root, executionPath);
|
|
1267
|
+
const selectedGit = gitDirectories(root, cwd, executionPath);
|
|
1268
|
+
if (protectedGit?.common === undefined ||
|
|
1269
|
+
protectedGit.common !== grantRepository?.common ||
|
|
1270
|
+
protectedGit.worktree !== grantRepository.worktree ||
|
|
1271
|
+
selectedGit?.common !== protectedGit.common ||
|
|
1272
|
+
selectedGit.worktree !== protectedGit.worktree)
|
|
1273
|
+
return undefined;
|
|
1274
|
+
configuredRemotes = protectedGitRemoteNames(root, protectedGit.common, executionPath);
|
|
1275
|
+
if (configuredRemotes === undefined)
|
|
1276
|
+
return undefined;
|
|
1277
|
+
}
|
|
1278
|
+
const matched = execRules.match([request.executable, ...request.args], configuredRemotes);
|
|
1279
|
+
if (matched === undefined)
|
|
1280
|
+
return undefined;
|
|
1281
|
+
input.onExecRuleMatch?.({
|
|
1282
|
+
agentId,
|
|
1283
|
+
executable: request.executable,
|
|
1284
|
+
ruleId: matched.ruleId,
|
|
1285
|
+
});
|
|
1286
|
+
return shellOutput.parse(await dispatchApprovedLocalExec(request, cwd, executionPath, configRoot.home, localTools.processes.scope(sessionId), mode));
|
|
1287
|
+
};
|
|
1288
|
+
const localTools = await createPrivateLocalTools(selectedFolder.absolute, input.fungiMcp, subagents?.directory, (sessionId) => sessionPaths.get(sessionId), dirname(paths.databasePath), subagents, await input.configuration?.(dirname(paths.databasePath)), configRoot, sandboxOwner, () => sandboxOwner.status().mode, escapeForSession, policyForSession);
|
|
1019
1289
|
processes = localTools.processes;
|
|
1020
1290
|
const sessionProcesses = processes;
|
|
1021
1291
|
const modelSource = createPrivateLocalModelSource(models, selectedProviderId, selectedModelId, providerCacheKey, input.reasoning, { agent: readAgentModel });
|
|
@@ -1041,6 +1311,40 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1041
1311
|
readSettlement = host.readSettlement;
|
|
1042
1312
|
}
|
|
1043
1313
|
const sessionOwner = agent;
|
|
1314
|
+
async function recoverEscapes(sessionId) {
|
|
1315
|
+
for (const recordId of escapeLedger.pendingApplications())
|
|
1316
|
+
await escapeLedger.apply(recordId, escapeApply);
|
|
1317
|
+
for (const recordId of escapeLedger.pendingHandoffs())
|
|
1318
|
+
await deliverEscapeResult(recordId);
|
|
1319
|
+
const page = await escapeApprovals.list({ state: "pending", limit: 100 });
|
|
1320
|
+
for (const view of page.items) {
|
|
1321
|
+
if (view.record.requester.sessionId !== sessionId)
|
|
1322
|
+
continue;
|
|
1323
|
+
if (view.record.module !== "computer" ||
|
|
1324
|
+
view.record.operation !== "exec")
|
|
1325
|
+
continue;
|
|
1326
|
+
const input = escapeLedger.readInput(view.record.recordId);
|
|
1327
|
+
const evidence = z
|
|
1328
|
+
.object({
|
|
1329
|
+
request: z.object({
|
|
1330
|
+
command: z.string().optional(),
|
|
1331
|
+
executable: z.string().optional(),
|
|
1332
|
+
args: z.array(z.string()).optional(),
|
|
1333
|
+
escape: z.object({ justification: z.string() }),
|
|
1334
|
+
}),
|
|
1335
|
+
cwd: z.string(),
|
|
1336
|
+
})
|
|
1337
|
+
.parse(input);
|
|
1338
|
+
const review = localExecEscapeReview(evidence.request, evidence.cwd);
|
|
1339
|
+
await localTools.presentApproval(sessionId, {
|
|
1340
|
+
kind: "pending_approval",
|
|
1341
|
+
recordId: view.record.recordId,
|
|
1342
|
+
description: view.record.description,
|
|
1343
|
+
}, escapeApprovals, review, review.prefix === undefined
|
|
1344
|
+
? undefined
|
|
1345
|
+
: selectRule(view.record.recordId, review.prefix));
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1044
1348
|
const setAgentModel = async (input, signal) => {
|
|
1045
1349
|
const preference = parseModelPreference(input);
|
|
1046
1350
|
if (preference === null)
|
|
@@ -1053,6 +1357,10 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1053
1357
|
// Session override is cleared so the running Session follows the default.
|
|
1054
1358
|
const getSession = (sessionId, attachment) => {
|
|
1055
1359
|
const session = localTools.bindSession(sessionId, sessionOwner.session(sessionId), attachment);
|
|
1360
|
+
if (attachment !== undefined)
|
|
1361
|
+
void recoverEscapes(sessionId).catch((cause) => {
|
|
1362
|
+
logEvent("local-approval-recovery-failed", cause);
|
|
1363
|
+
});
|
|
1056
1364
|
return {
|
|
1057
1365
|
...session,
|
|
1058
1366
|
modelPreference: {
|
|
@@ -1076,11 +1384,11 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1076
1384
|
getSession,
|
|
1077
1385
|
presentApproval: localTools.presentApproval,
|
|
1078
1386
|
setModel: setAgentModel,
|
|
1079
|
-
|
|
1387
|
+
setMode: (mode) => {
|
|
1388
|
+
sandboxOwner.assertModeReady(mode);
|
|
1080
1389
|
sandboxDatabase
|
|
1081
|
-
.prepare("UPDATE private_local_identity SET
|
|
1082
|
-
.run(
|
|
1083
|
-
sandboxOwner.assertReady();
|
|
1390
|
+
.prepare("UPDATE private_local_identity SET mode = ? WHERE singleton = 1")
|
|
1391
|
+
.run(mode);
|
|
1084
1392
|
},
|
|
1085
1393
|
sandboxStatus: sandboxOwner.status,
|
|
1086
1394
|
setArchived: (archived) => {
|
|
@@ -1140,10 +1448,12 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1140
1448
|
? Promise.reject(new Error("Local settlement reads are unavailable"))
|
|
1141
1449
|
: readSettlement(sessionId, commandId),
|
|
1142
1450
|
idle: async () => {
|
|
1451
|
+
const busy = { idle: false, wakeAt: null };
|
|
1143
1452
|
if (sessionProcesses.running())
|
|
1144
|
-
return
|
|
1145
|
-
|
|
1146
|
-
|
|
1453
|
+
return busy;
|
|
1454
|
+
const settlement = await runtime.settled();
|
|
1455
|
+
if (!settlement.idle)
|
|
1456
|
+
return busy;
|
|
1147
1457
|
// Delegated work still owed to this Agent is not read here: that read
|
|
1148
1458
|
// reopens each child, and the child's reply mail reopens this Agent.
|
|
1149
1459
|
let after;
|
|
@@ -1151,7 +1461,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1151
1461
|
const page = await sessionOwner.listSessions(after);
|
|
1152
1462
|
for (const row of page.sessions)
|
|
1153
1463
|
if (row.state === "running" || row.state === "needs_you")
|
|
1154
|
-
return
|
|
1464
|
+
return busy;
|
|
1155
1465
|
after = page.next;
|
|
1156
1466
|
} while (after !== undefined);
|
|
1157
1467
|
// Reconciliation enqueues replies before advancing its settled cursor.
|
|
@@ -1159,8 +1469,11 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1159
1469
|
// while we await settlement or Session reads, especially during stop
|
|
1160
1470
|
// when delivery cannot reopen a parent. The owner rechecks admissions
|
|
1161
1471
|
// and uses after this observation before forgetting the Agent.
|
|
1162
|
-
return
|
|
1163
|
-
|
|
1472
|
+
return {
|
|
1473
|
+
idle: input.mail === undefined ||
|
|
1474
|
+
!(await hasPrivateLocalPendingMail(modelDatabase)),
|
|
1475
|
+
wakeAt: settlement.wakeAt,
|
|
1476
|
+
};
|
|
1164
1477
|
},
|
|
1165
1478
|
close: () => {
|
|
1166
1479
|
closing ??= closePrivateLocalResources({
|