@fungi.computer/truffle 0.2.1 → 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 +39 -36
- package/dist/private-invocation-computer.d.ts +224 -5
- 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 +17 -7
- package/dist/private-local-bwrap.js +73 -33
- 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 +18 -8
- package/dist/private-local-containment.js +70 -45
- 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 -3
- package/dist/private-local-linux-sandbox.js +71 -25
- 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 +2 -0
- 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 +16 -0
- package/dist/private-local-native-staging.js +138 -0
- 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 +391 -71
- package/dist/private-local-sandbox-policy.d.ts +46 -6
- package/dist/private-local-sandbox-policy.js +350 -71
- package/dist/private-local-sandbox.d.ts +34 -18
- package/dist/private-local-sandbox.js +225 -60
- 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 +18 -6
- 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 +43 -9
- package/dist/private-local-session-discovery.d.ts +3 -2
- 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 +3 -1
- 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 +25 -19
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai/utils/event-stream";
|
|
2
|
-
import { observeProviderStatus } from "@fungi.computer/mule";
|
|
2
|
+
import { observeProviderStatus, normalizeModel, decodeProviderData, } from "@fungi.computer/mule";
|
|
3
|
+
import { normalizeContext } from "@earendil-works/pi-ai/utils/transcript";
|
|
3
4
|
import { modelFailureMessage, modelFailureRetryable, } from "./private-provider-rejection.js";
|
|
4
5
|
import { logEvent } from "./log-event.js";
|
|
5
6
|
import { localArchiveHandoff } from "./local-archive-handoff.js";
|
|
6
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";
|
|
7
10
|
import { localExtensions, } from "./local-extensions.js";
|
|
8
11
|
import { lstat, mkdir, realpath, stat } from "node:fs/promises";
|
|
9
12
|
import { lstatSync, mkdirSync, realpathSync } from "node:fs";
|
|
10
13
|
import { basename, dirname, isAbsolute, join, relative, resolve, } from "node:path";
|
|
11
14
|
import { createHash } from "node:crypto";
|
|
12
15
|
import { z } from "zod";
|
|
16
|
+
import { defaultLocalPermissionMode, localPermissionModeSchema, modeWithinCeiling, } from "./private-local-permission-mode.js";
|
|
13
17
|
import { cleanupSessionResources, } from "@earendil-works/pi-ai";
|
|
14
18
|
import { parseModelPreference, } from "@fungi.computer/shiitake/model-preference";
|
|
15
|
-
import { createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
|
|
19
|
+
import { AgentCreateRefused, createAgentControlSource, } from "@fungi.computer/shiitake/agent-control";
|
|
16
20
|
import { defineIdFamily } from "@fungi.computer/cairn";
|
|
17
21
|
import { words } from "@fungi.computer/cairn/words";
|
|
18
22
|
import { botanicalExtensionsWithSkills } from "@fungi.computer/shiitake-extensions/roster";
|
|
@@ -27,12 +31,14 @@ import { ensureAgentDefaultModelTable, readAgentDefaultModel, seedAgentDefaultMo
|
|
|
27
31
|
import { defaultLocalReasoning, resolveKnownLocalStartupChoice, resolveLocalStartupChoice, startupReasoning, validateLocalReasoning, } from "./private-local-startup-model.js";
|
|
28
32
|
import { assertFungiModelsEnabled } from "./private-fungi-model-api.js";
|
|
29
33
|
import { privateLocalEffectiveModelSchema, } from "./private-local-effective-model.js";
|
|
30
|
-
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";
|
|
31
36
|
import { createPrivateLocalComputerProcesses, } from "./private-local-computer-processes.js";
|
|
32
37
|
import { capturePrivateLocalExecPath, parsePrivateLocalExecPath, } from "./private-local-exec-path.js";
|
|
33
38
|
import { ensurePrivateLocalGitExclude } from "./private-local-git-exclude.js";
|
|
34
|
-
import { createLocalSandboxControls, } from "./private-local-sandbox.js";
|
|
35
|
-
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";
|
|
36
42
|
import { LocalStoreSchemaError } from "./private-local-session-discovery.js";
|
|
37
43
|
import { acquireNodeStoreLease, nodeRuntimeHost, } from "@fungi.computer/shiitake/node";
|
|
38
44
|
import { createPrivateInvocationFilesPort, } from "./private-invocation-files.js";
|
|
@@ -64,7 +70,8 @@ function isErrnoException(error) {
|
|
|
64
70
|
function sidecarPathFor(databasePath) {
|
|
65
71
|
return `${databasePath}.owner.sqlite`;
|
|
66
72
|
}
|
|
67
|
-
function ensureLocalAgentId(
|
|
73
|
+
function ensureLocalAgentId(options) {
|
|
74
|
+
const { database, databasePath, project, pathAgentId, name, createdBy, folder, mode, native, } = options;
|
|
68
75
|
database.exec("BEGIN IMMEDIATE");
|
|
69
76
|
try {
|
|
70
77
|
database.exec(`
|
|
@@ -75,16 +82,24 @@ function ensureLocalAgentId(database, databasePath, pathAgentId, name, createdBy
|
|
|
75
82
|
created_by TEXT,
|
|
76
83
|
archived INTEGER NOT NULL DEFAULT 0 CHECK (archived IN (0, 1)),
|
|
77
84
|
folder TEXT NOT NULL DEFAULT '.',
|
|
78
|
-
|
|
85
|
+
mode TEXT NOT NULL CHECK (mode IN ('full-access', 'workspace', 'read-only')),
|
|
86
|
+
writable_folders TEXT NOT NULL DEFAULT '[]'
|
|
79
87
|
)
|
|
80
88
|
`);
|
|
81
89
|
const columns = database
|
|
82
90
|
.prepare("PRAGMA table_info(private_local_identity)")
|
|
83
91
|
.all();
|
|
84
|
-
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)))
|
|
85
100
|
throw new LocalStoreSchemaError(databasePath);
|
|
86
101
|
const row = database
|
|
87
|
-
.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")
|
|
88
103
|
.get();
|
|
89
104
|
if (row !== undefined) {
|
|
90
105
|
const parsed = localHostIds.store.schema.safeParse(row.agent_id);
|
|
@@ -97,18 +112,37 @@ function ensureLocalAgentId(database, databasePath, pathAgentId, name, createdBy
|
|
|
97
112
|
if (folder !== undefined &&
|
|
98
113
|
localAgentFolderSchema.parse(folder) !== storedFolder)
|
|
99
114
|
throw new Error("Local Agent folder conflicts with its store");
|
|
100
|
-
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
|
+
}
|
|
101
127
|
database
|
|
102
|
-
.prepare("UPDATE private_local_identity SET
|
|
103
|
-
.run(
|
|
128
|
+
.prepare("UPDATE private_local_identity SET mode = ? WHERE singleton = 1")
|
|
129
|
+
.run(mode);
|
|
130
|
+
}
|
|
104
131
|
database.exec("COMMIT");
|
|
105
132
|
return parsed.data;
|
|
106
133
|
}
|
|
107
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);
|
|
108
142
|
database
|
|
109
|
-
.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, ?, ?, ?, ?, ?, '[]')")
|
|
110
144
|
// An unnamed Agent gets a Cairn candidate word name so it never shows its id.
|
|
111
|
-
.run(agentId, name ?? words(), createdBy ?? null,
|
|
145
|
+
.run(agentId, name ?? words(), createdBy ?? null, selectedFolder, selectedMode);
|
|
112
146
|
database.exec("COMMIT");
|
|
113
147
|
return agentId;
|
|
114
148
|
}
|
|
@@ -556,82 +590,122 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
556
590
|
// prose that any SDK can supply.
|
|
557
591
|
let modelFailure;
|
|
558
592
|
return Promise.resolve(Object.freeze({
|
|
559
|
-
model: selected,
|
|
593
|
+
model: normalizeModel(selected),
|
|
560
594
|
retryable: (message) => !signal.aborted &&
|
|
561
595
|
message.stopReason === "error" &&
|
|
562
596
|
modelFailure?.message === message &&
|
|
563
597
|
modelFailure.retryable,
|
|
564
598
|
failureReason: (message) => modelFailure?.message === message ? modelFailure.reason : undefined,
|
|
599
|
+
providerError: (message) => modelFailure?.message === message
|
|
600
|
+
? modelFailure.providerError
|
|
601
|
+
: undefined,
|
|
565
602
|
stream: (selected, context, options) => {
|
|
603
|
+
const startedAt = Date.now();
|
|
566
604
|
modelFailure = undefined;
|
|
605
|
+
let lastPartial;
|
|
567
606
|
const streamOptions = { ...options };
|
|
568
607
|
const reasoning = options?.reasoning ?? selectedReasoning;
|
|
569
608
|
if (reasoning !== undefined)
|
|
570
609
|
streamOptions.reasoning = reasoning;
|
|
571
610
|
const provider = observeProviderStatus(streamOptions);
|
|
572
611
|
const events = createAssistantMessageEventStream();
|
|
573
|
-
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);
|
|
574
616
|
const evidence = {
|
|
575
617
|
refusal: turn.refusal(),
|
|
576
|
-
upstream,
|
|
618
|
+
upstream: upstream ?? partial,
|
|
577
619
|
rejection: provider.rejection(),
|
|
578
620
|
};
|
|
579
|
-
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.`;
|
|
580
624
|
const result = {
|
|
581
625
|
role: "assistant",
|
|
582
|
-
content: [],
|
|
626
|
+
content: partial?.content ?? [],
|
|
583
627
|
api: selected.api,
|
|
584
628
|
provider: selected.provider,
|
|
585
629
|
model: selected.id,
|
|
586
|
-
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
|
+
},
|
|
587
644
|
stopReason: aborted ? "aborted" : "error",
|
|
588
645
|
timestamp: Date.now(),
|
|
589
|
-
errorMessage: aborted ? "Run cancelled" : message,
|
|
590
646
|
};
|
|
591
|
-
|
|
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) {
|
|
592
651
|
modelFailure = {
|
|
593
652
|
message: result,
|
|
594
653
|
reason: message,
|
|
595
654
|
retryable: modelFailureRetryable(evidence),
|
|
596
655
|
};
|
|
656
|
+
if (upstream?.errorMessage !== undefined)
|
|
657
|
+
modelFailure.providerError = upstream.errorMessage;
|
|
658
|
+
}
|
|
597
659
|
return result;
|
|
598
660
|
};
|
|
599
661
|
void (async () => {
|
|
600
662
|
try {
|
|
601
|
-
const
|
|
602
|
-
|
|
663
|
+
const { onPayload, onResponse, ...rest } = streamOptions;
|
|
664
|
+
const requestOptions = {
|
|
665
|
+
...rest,
|
|
603
666
|
fetch: provider.fetch,
|
|
604
|
-
onProviderStreamEvent: provider.onProviderStreamEvent,
|
|
667
|
+
onProviderStreamEvent: (data, model) => provider.onProviderStreamEvent(decodeProviderData(data), normalizeModel(model)),
|
|
605
668
|
sessionId: providerCacheKey,
|
|
606
669
|
transport: options?.transport ?? "auto",
|
|
607
|
-
}
|
|
670
|
+
};
|
|
671
|
+
if (onPayload !== undefined)
|
|
672
|
+
requestOptions.onPayload = (data, model) => onPayload(decodeProviderData(data), normalizeModel(model));
|
|
673
|
+
if (onResponse !== undefined)
|
|
674
|
+
requestOptions.onResponse = (response, model) => onResponse(response, normalizeModel(model));
|
|
675
|
+
const upstream = turn.models.streamSimple(selected, normalizeContext(context), requestOptions);
|
|
608
676
|
for await (const event of upstream) {
|
|
677
|
+
if ("partial" in event)
|
|
678
|
+
lastPartial = event.partial;
|
|
609
679
|
if (event.type === "error") {
|
|
610
|
-
const error = failure(event.
|
|
680
|
+
const error = failure(event.reason === "aborted", event.error);
|
|
611
681
|
events.push({ ...event, error });
|
|
612
682
|
events.end(error);
|
|
613
683
|
return;
|
|
614
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
|
+
}
|
|
615
698
|
events.push(event);
|
|
616
699
|
}
|
|
617
|
-
|
|
700
|
+
const result = await upstream.result();
|
|
701
|
+
events.end(result.stopReason === "error" ||
|
|
702
|
+
result.stopReason === "aborted"
|
|
703
|
+
? failure(result.stopReason === "aborted", result)
|
|
704
|
+
: result);
|
|
618
705
|
}
|
|
619
|
-
catch {
|
|
706
|
+
catch (cause) {
|
|
620
707
|
const aborted = (options?.signal ?? signal).aborted;
|
|
621
|
-
const error = failure(
|
|
622
|
-
input: 0,
|
|
623
|
-
output: 0,
|
|
624
|
-
cacheRead: 0,
|
|
625
|
-
cacheWrite: 0,
|
|
626
|
-
totalTokens: 0,
|
|
627
|
-
cost: {
|
|
628
|
-
input: 0,
|
|
629
|
-
output: 0,
|
|
630
|
-
cacheRead: 0,
|
|
631
|
-
cacheWrite: 0,
|
|
632
|
-
total: 0,
|
|
633
|
-
},
|
|
634
|
-
}, aborted);
|
|
708
|
+
const error = failure(aborted, undefined, cause);
|
|
635
709
|
events.push({
|
|
636
710
|
type: "error",
|
|
637
711
|
reason: aborted ? "aborted" : "error",
|
|
@@ -648,7 +722,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
648
722
|
});
|
|
649
723
|
}
|
|
650
724
|
/** One local Computer source, resolving its PATH and process scope per Session. */
|
|
651
|
-
function createPrivateLocalComputerSource(root, port, sessionPath, processes, home, sandbox) {
|
|
725
|
+
function createPrivateLocalComputerSource(root, port, sessionPath, processes, home, sandbox, mode = () => "full-access", escapeForSession, policyForSession) {
|
|
652
726
|
const id = "local-computer";
|
|
653
727
|
return {
|
|
654
728
|
id,
|
|
@@ -658,7 +732,7 @@ function createPrivateLocalComputerSource(root, port, sessionPath, processes, ho
|
|
|
658
732
|
throw new Error("Local Computer requires a Session");
|
|
659
733
|
const path = sessionPath(sessionId) ??
|
|
660
734
|
capturePrivateLocalExecPath(process.env.PATH ?? "/usr/bin:/bin", process.cwd());
|
|
661
|
-
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));
|
|
662
736
|
return Knapsack.computer({
|
|
663
737
|
id,
|
|
664
738
|
name: "local",
|
|
@@ -683,10 +757,10 @@ storeDirectory,
|
|
|
683
757
|
* This Agent's sub-agent record: its Session-bound Agent-control tools and
|
|
684
758
|
* the view's facts. Present when it can direct other Agents.
|
|
685
759
|
*/
|
|
686
|
-
subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox) {
|
|
760
|
+
subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox, currentMode = () => "full-access", escapeForSession, policyForSession) {
|
|
687
761
|
const forms = createMcpFormSession();
|
|
688
762
|
const root = await realpath(resolve(invocationRoot));
|
|
689
|
-
const port = await createPrivateInvocationFilesPort(root, undefined, sandbox);
|
|
763
|
+
const port = await createPrivateInvocationFilesPort(root, undefined, sandbox, currentMode);
|
|
690
764
|
const processes = createPrivateLocalComputerProcesses(root, configRoot.home);
|
|
691
765
|
const computerSources = sessionPath === undefined
|
|
692
766
|
? [
|
|
@@ -698,11 +772,11 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox)
|
|
|
698
772
|
port,
|
|
699
773
|
pathInstructions: localPathInstructions(root),
|
|
700
774
|
},
|
|
701
|
-
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")),
|
|
702
776
|
}),
|
|
703
777
|
]
|
|
704
778
|
: [
|
|
705
|
-
createPrivateLocalComputerSource(root, port, sessionPath, processes, configRoot.home, sandbox?.launch),
|
|
779
|
+
createPrivateLocalComputerSource(root, port, sessionPath, processes, configRoot.home, sandbox?.launch, currentMode, escapeForSession, policyForSession),
|
|
706
780
|
];
|
|
707
781
|
const installed = configuration?.installed ??
|
|
708
782
|
(storeDirectory === undefined
|
|
@@ -726,7 +800,11 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox)
|
|
|
726
800
|
homeExtension({
|
|
727
801
|
computers: {
|
|
728
802
|
read: () => [
|
|
729
|
-
{
|
|
803
|
+
{
|
|
804
|
+
name: "local",
|
|
805
|
+
attachmentId: "local-computer",
|
|
806
|
+
port,
|
|
807
|
+
},
|
|
730
808
|
],
|
|
731
809
|
},
|
|
732
810
|
}),
|
|
@@ -759,7 +837,7 @@ subagents, configuration, configRoot = capturePrivateLocalConfigRoot(), sandbox)
|
|
|
759
837
|
...(clientPath === undefined
|
|
760
838
|
? computerSources
|
|
761
839
|
: [
|
|
762
|
-
createPrivateLocalComputerSource(root, port, () => clientPath, processes, configRoot.home, sandbox?.launch),
|
|
840
|
+
createPrivateLocalComputerSource(root, port, () => clientPath, processes, configRoot.home, sandbox?.launch, currentMode, escapeForSession, policyForSession),
|
|
763
841
|
]),
|
|
764
842
|
...(connection === undefined
|
|
765
843
|
? []
|
|
@@ -797,9 +875,13 @@ function localRuntimeOwner() {
|
|
|
797
875
|
}),
|
|
798
876
|
settled: async () => {
|
|
799
877
|
if (pendingWork === undefined)
|
|
800
|
-
return false;
|
|
878
|
+
return { idle: false, wakeAt: null };
|
|
801
879
|
const work = await pendingWork();
|
|
802
|
-
return
|
|
880
|
+
return {
|
|
881
|
+
idle: !work.pending &&
|
|
882
|
+
(work.nextWakeAt === null || work.nextWakeAt > Date.now()),
|
|
883
|
+
wakeAt: work.nextWakeAt,
|
|
884
|
+
};
|
|
803
885
|
},
|
|
804
886
|
});
|
|
805
887
|
}
|
|
@@ -910,6 +992,16 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
910
992
|
throw new ShiitakeError("model", "rejected", `Local model is not registered: ${selectedProviderId}/${selectedModelId ?? "select with --model"}`);
|
|
911
993
|
validateLocalReasoning(model, input.reasoning);
|
|
912
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
|
+
}
|
|
913
1005
|
const paths = await localDatabasePaths(root, input.databasePath, input.createRequestId);
|
|
914
1006
|
const runtime = localRuntimeOwner();
|
|
915
1007
|
let lease;
|
|
@@ -928,20 +1020,34 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
928
1020
|
.digest("hex");
|
|
929
1021
|
database = openPrivateWritableSqliteDatabase(paths.databasePath, 0);
|
|
930
1022
|
database.exec("PRAGMA foreign_keys = ON");
|
|
931
|
-
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
|
+
});
|
|
932
1034
|
const sandboxDatabase = database;
|
|
933
|
-
const
|
|
934
|
-
.
|
|
935
|
-
.
|
|
936
|
-
.prepare("SELECT require_sandbox FROM private_local_identity WHERE singleton = 1")
|
|
937
|
-
.get()?.require_sandbox) === 1;
|
|
1035
|
+
const readMode = () => localPermissionModeSchema.parse(sandboxDatabase
|
|
1036
|
+
.prepare("SELECT mode FROM private_local_identity WHERE singleton = 1")
|
|
1037
|
+
.get()?.mode);
|
|
938
1038
|
const selectedFolder = resolveAgentFolder(root, localAgentFolderSchema.parse(database
|
|
939
1039
|
.prepare("SELECT folder FROM private_local_identity WHERE singleton = 1")
|
|
940
1040
|
.get()?.folder));
|
|
1041
|
+
const defaultReason = readMode() === "full-access" && input.mode === undefined
|
|
1042
|
+
? probeLocalSandbox(root, selectedFolder.folder, input.native)
|
|
1043
|
+
: undefined;
|
|
941
1044
|
const sandboxOwner = createLocalSandboxControls({
|
|
942
1045
|
project: root,
|
|
943
1046
|
folder: selectedFolder.folder,
|
|
944
|
-
|
|
1047
|
+
agentId,
|
|
1048
|
+
mode: readMode,
|
|
1049
|
+
defaultReason,
|
|
1050
|
+
native: input.native,
|
|
945
1051
|
});
|
|
946
1052
|
sandbox = sandboxOwner;
|
|
947
1053
|
// Both constructors below can resume stored commands before returning.
|
|
@@ -1008,7 +1114,178 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1008
1114
|
directory: projectDirectory,
|
|
1009
1115
|
storage: store,
|
|
1010
1116
|
});
|
|
1011
|
-
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);
|
|
1012
1289
|
processes = localTools.processes;
|
|
1013
1290
|
const sessionProcesses = processes;
|
|
1014
1291
|
const modelSource = createPrivateLocalModelSource(models, selectedProviderId, selectedModelId, providerCacheKey, input.reasoning, { agent: readAgentModel });
|
|
@@ -1034,6 +1311,40 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1034
1311
|
readSettlement = host.readSettlement;
|
|
1035
1312
|
}
|
|
1036
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
|
+
}
|
|
1037
1348
|
const setAgentModel = async (input, signal) => {
|
|
1038
1349
|
const preference = parseModelPreference(input);
|
|
1039
1350
|
if (preference === null)
|
|
@@ -1046,6 +1357,10 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1046
1357
|
// Session override is cleared so the running Session follows the default.
|
|
1047
1358
|
const getSession = (sessionId, attachment) => {
|
|
1048
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
|
+
});
|
|
1049
1364
|
return {
|
|
1050
1365
|
...session,
|
|
1051
1366
|
modelPreference: {
|
|
@@ -1069,11 +1384,11 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1069
1384
|
getSession,
|
|
1070
1385
|
presentApproval: localTools.presentApproval,
|
|
1071
1386
|
setModel: setAgentModel,
|
|
1072
|
-
|
|
1387
|
+
setMode: (mode) => {
|
|
1388
|
+
sandboxOwner.assertModeReady(mode);
|
|
1073
1389
|
sandboxDatabase
|
|
1074
|
-
.prepare("UPDATE private_local_identity SET
|
|
1075
|
-
.run(
|
|
1076
|
-
sandboxOwner.assertReady();
|
|
1390
|
+
.prepare("UPDATE private_local_identity SET mode = ? WHERE singleton = 1")
|
|
1391
|
+
.run(mode);
|
|
1077
1392
|
},
|
|
1078
1393
|
sandboxStatus: sandboxOwner.status,
|
|
1079
1394
|
setArchived: (archived) => {
|
|
@@ -1133,10 +1448,12 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1133
1448
|
? Promise.reject(new Error("Local settlement reads are unavailable"))
|
|
1134
1449
|
: readSettlement(sessionId, commandId),
|
|
1135
1450
|
idle: async () => {
|
|
1451
|
+
const busy = { idle: false, wakeAt: null };
|
|
1136
1452
|
if (sessionProcesses.running())
|
|
1137
|
-
return
|
|
1138
|
-
|
|
1139
|
-
|
|
1453
|
+
return busy;
|
|
1454
|
+
const settlement = await runtime.settled();
|
|
1455
|
+
if (!settlement.idle)
|
|
1456
|
+
return busy;
|
|
1140
1457
|
// Delegated work still owed to this Agent is not read here: that read
|
|
1141
1458
|
// reopens each child, and the child's reply mail reopens this Agent.
|
|
1142
1459
|
let after;
|
|
@@ -1144,7 +1461,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1144
1461
|
const page = await sessionOwner.listSessions(after);
|
|
1145
1462
|
for (const row of page.sessions)
|
|
1146
1463
|
if (row.state === "running" || row.state === "needs_you")
|
|
1147
|
-
return
|
|
1464
|
+
return busy;
|
|
1148
1465
|
after = page.next;
|
|
1149
1466
|
} while (after !== undefined);
|
|
1150
1467
|
// Reconciliation enqueues replies before advancing its settled cursor.
|
|
@@ -1152,8 +1469,11 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1152
1469
|
// while we await settlement or Session reads, especially during stop
|
|
1153
1470
|
// when delivery cannot reopen a parent. The owner rechecks admissions
|
|
1154
1471
|
// and uses after this observation before forgetting the Agent.
|
|
1155
|
-
return
|
|
1156
|
-
|
|
1472
|
+
return {
|
|
1473
|
+
idle: input.mail === undefined ||
|
|
1474
|
+
!(await hasPrivateLocalPendingMail(modelDatabase)),
|
|
1475
|
+
wakeAt: settlement.wakeAt,
|
|
1476
|
+
};
|
|
1157
1477
|
},
|
|
1158
1478
|
close: () => {
|
|
1159
1479
|
closing ??= closePrivateLocalResources({
|