@fungi.computer/truffle 0.2.2 → 0.3.1
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 +12 -0
- package/dist/private-local-exec-review.js +62 -0
- package/dist/private-local-exec-rules.d.ts +16 -0
- package/dist/private-local-exec-rules.js +441 -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 +28 -5
- package/dist/private-local-runtime-host.js +404 -66
- 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 +18 -0
- package/dist/private-local-service-log.js +6 -0
- package/dist/private-local-service-owner.d.ts +18 -10
- package/dist/private-local-service-owner.js +119 -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 +10 -1
- package/dist/private-mcp-form-contract.js +10 -1
- package/dist/private-mcp-form-session.d.ts +39 -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
|
}
|
|
@@ -433,7 +466,7 @@ export function createPrivateLocalModelSource(models, providerId, modelId, provi
|
|
|
433
466
|
signal,
|
|
434
467
|
});
|
|
435
468
|
if (startup === null)
|
|
436
|
-
throw new ShiitakeError("model", "rejected", "No signed-in model provider is available.
|
|
469
|
+
throw new ShiitakeError("model", "rejected", "No signed-in model provider is available. Use your own Claude or ChatGPT subscription: /login in a Session, or fungi login --provider anthropic --auth-type oauth (Claude) or fungi login --provider openai --auth-type oauth (ChatGPT) in the CLI. For hosted Fungi: fungi login --provider fungi.");
|
|
437
470
|
const choice = startup.preference;
|
|
438
471
|
const selectedProvider = choice.providerId;
|
|
439
472
|
if (selectedProvider === "fungi") {
|
|
@@ -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,203 @@ 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
|
+
const gitGrantFor = (cwd, executionPath) => {
|
|
1129
|
+
// A nested/Agent-created repository has writable config and hooks.
|
|
1130
|
+
const protectedGit = grantRepository === undefined
|
|
1131
|
+
? undefined
|
|
1132
|
+
: gitDirectories(root, root, executionPath);
|
|
1133
|
+
const selectedGit = gitDirectories(root, cwd, executionPath);
|
|
1134
|
+
if (protectedGit?.common === undefined ||
|
|
1135
|
+
protectedGit.common !== grantRepository?.common ||
|
|
1136
|
+
protectedGit.worktree !== grantRepository.worktree ||
|
|
1137
|
+
selectedGit?.common !== protectedGit.common ||
|
|
1138
|
+
selectedGit.worktree !== protectedGit.worktree)
|
|
1139
|
+
return undefined;
|
|
1140
|
+
const configuredRemotes = protectedGitRemoteNames(root, protectedGit.common, executionPath);
|
|
1141
|
+
return configuredRemotes === undefined
|
|
1142
|
+
? undefined
|
|
1143
|
+
: { configuredRemotes };
|
|
1144
|
+
};
|
|
1145
|
+
// The attached human's answer, never form JSON or the model, selects
|
|
1146
|
+
// this short-lived intent. Commit the rule before dispatching the command.
|
|
1147
|
+
const ruleIntents = new Map();
|
|
1148
|
+
const selectRule = (recordId, prefix) => (selected) => {
|
|
1149
|
+
if (selected === "prefix")
|
|
1150
|
+
ruleIntents.set(recordId, () => execRules.add(prefix));
|
|
1151
|
+
else
|
|
1152
|
+
ruleIntents.delete(recordId);
|
|
1153
|
+
};
|
|
1154
|
+
const escapeFence = () => JSON.stringify([
|
|
1155
|
+
readMode(),
|
|
1156
|
+
resolveAgentFolder(root, localAgentFolderSchema.parse(sandboxDatabase
|
|
1157
|
+
.prepare("SELECT folder FROM private_local_identity WHERE singleton = 1")
|
|
1158
|
+
.get()?.folder)).absolute,
|
|
1159
|
+
]);
|
|
1160
|
+
const escapeApply = {
|
|
1161
|
+
recheck: () => Promise.resolve({
|
|
1162
|
+
kind: "authorized",
|
|
1163
|
+
authorityFence: escapeFence(),
|
|
1164
|
+
}),
|
|
1165
|
+
dispatch: async ({ record, input: payload }) => {
|
|
1166
|
+
if (record.module !== "computer" || record.operation !== "exec")
|
|
1167
|
+
return { kind: "refused", reason: "not-granted" };
|
|
1168
|
+
const evidence = z
|
|
1169
|
+
.object({
|
|
1170
|
+
request: z.unknown(),
|
|
1171
|
+
cwd: z.string(),
|
|
1172
|
+
executionPath: z.string(),
|
|
1173
|
+
})
|
|
1174
|
+
.parse(payload);
|
|
1175
|
+
// Refuse a replaced or re-pointed working directory: the card showed
|
|
1176
|
+
// the canonical path selected at staging, not a later symlink target.
|
|
1177
|
+
if ((await realpath(evidence.cwd)) !== evidence.cwd)
|
|
1178
|
+
return { kind: "refused", reason: "not-granted" };
|
|
1179
|
+
const sessionId = record.requester.sessionId;
|
|
1180
|
+
if (sessionId === null)
|
|
1181
|
+
return { kind: "refused", reason: "not-granted" };
|
|
1182
|
+
const output = await dispatchApprovedLocalExec(evidence.request, evidence.cwd, evidence.executionPath, configRoot.home, localTools.processes.scope(sessionId), readMode());
|
|
1183
|
+
return { kind: "completed", output: z.json().parse(output) };
|
|
1184
|
+
},
|
|
1185
|
+
};
|
|
1186
|
+
const deliverEscapeResult = async (recordId) => {
|
|
1187
|
+
await escapeLedger.handoff(recordId, async (notice) => {
|
|
1188
|
+
const sessionId = notice.record.requester.sessionId;
|
|
1189
|
+
if (sessionId === null)
|
|
1190
|
+
throw new Error("Approval has no recipient Session");
|
|
1191
|
+
const accepted = await sessionOwner
|
|
1192
|
+
.session(sessionId)
|
|
1193
|
+
.send(`Approval ${notice.record.module}.${notice.record.operation}: ${JSON.stringify(notice.result)}`, { requestId: notice.deliveryId, delivery: "followUp" });
|
|
1194
|
+
return { commandId: accepted.commandId };
|
|
1195
|
+
});
|
|
1196
|
+
};
|
|
1197
|
+
const escapeApprovals = escapeLedger.capability({
|
|
1198
|
+
// Only the attached Local UI reaches the form's answer command. The
|
|
1199
|
+
// model has no member-bound approval capability.
|
|
1200
|
+
actor: () => Promise.resolve({
|
|
1201
|
+
kind: "human",
|
|
1202
|
+
memberId: agentId,
|
|
1203
|
+
role: "owner",
|
|
1204
|
+
actingForAgent: false,
|
|
1205
|
+
mayUseOperation: true,
|
|
1206
|
+
}),
|
|
1207
|
+
decision: async (recordId) => {
|
|
1208
|
+
const answered = escapeLedger.get(recordId);
|
|
1209
|
+
if (answered?.state === "approved") {
|
|
1210
|
+
let ruleId;
|
|
1211
|
+
if (answered.module === "computer" &&
|
|
1212
|
+
answered.operation === "exec" &&
|
|
1213
|
+
answered.authorityFence === escapeFence())
|
|
1214
|
+
ruleId = ruleIntents.get(recordId)?.();
|
|
1215
|
+
if (answered.module === "computer" && answered.operation === "exec") {
|
|
1216
|
+
const event = {
|
|
1217
|
+
agentId,
|
|
1218
|
+
recordId,
|
|
1219
|
+
decision: "approve",
|
|
1220
|
+
scope: ruleIntents.has(recordId) ? "always" : "once",
|
|
1221
|
+
};
|
|
1222
|
+
if (ruleId !== undefined)
|
|
1223
|
+
event.ruleId = ruleId;
|
|
1224
|
+
input.onExecApprovalDecision?.(event);
|
|
1225
|
+
}
|
|
1226
|
+
await escapeLedger.apply(recordId, escapeApply);
|
|
1227
|
+
}
|
|
1228
|
+
if (answered?.state === "denied" &&
|
|
1229
|
+
answered.module === "computer" &&
|
|
1230
|
+
answered.operation === "exec")
|
|
1231
|
+
input.onExecApprovalDecision?.({
|
|
1232
|
+
agentId,
|
|
1233
|
+
recordId,
|
|
1234
|
+
decision: "deny",
|
|
1235
|
+
});
|
|
1236
|
+
for (const pending of escapeLedger.pendingHandoffs())
|
|
1237
|
+
await deliverEscapeResult(pending);
|
|
1238
|
+
},
|
|
1239
|
+
});
|
|
1240
|
+
const escapeForSession = (sessionId) => async ({ request, cwd, mode, denialLine, context, executionPath }) => {
|
|
1241
|
+
if (request.escape === undefined)
|
|
1242
|
+
throw new Error("An escape requires a justification");
|
|
1243
|
+
// The identity, current mode and root are host facts, not model input.
|
|
1244
|
+
const staged = await escapeLedger.request({
|
|
1245
|
+
requester: {
|
|
1246
|
+
principalId: agentId,
|
|
1247
|
+
agentId,
|
|
1248
|
+
sessionId,
|
|
1249
|
+
displayName: name,
|
|
1250
|
+
},
|
|
1251
|
+
module: "computer",
|
|
1252
|
+
operation: "exec",
|
|
1253
|
+
access: "external",
|
|
1254
|
+
reversibility: "irreversible-external",
|
|
1255
|
+
authorityFence: JSON.stringify([mode, selectedFolder.absolute]),
|
|
1256
|
+
idempotencyKey: JSON.stringify([
|
|
1257
|
+
context.executionId,
|
|
1258
|
+
context.toolCallId ?? null,
|
|
1259
|
+
context.operationOrdinal ?? null,
|
|
1260
|
+
]),
|
|
1261
|
+
inputJson: JSON.stringify({
|
|
1262
|
+
request,
|
|
1263
|
+
cwd,
|
|
1264
|
+
mode,
|
|
1265
|
+
denialLine,
|
|
1266
|
+
executionPath,
|
|
1267
|
+
}),
|
|
1268
|
+
requireHumanApproval: true,
|
|
1269
|
+
});
|
|
1270
|
+
if (staged.kind === "refused")
|
|
1271
|
+
return staged;
|
|
1272
|
+
if (staged.record.state !== "pending")
|
|
1273
|
+
return {
|
|
1274
|
+
kind: "refused",
|
|
1275
|
+
reason: staged.record.state === "expired"
|
|
1276
|
+
? "expired"
|
|
1277
|
+
: "already-answered",
|
|
1278
|
+
};
|
|
1279
|
+
const pending = {
|
|
1280
|
+
kind: "pending_approval",
|
|
1281
|
+
recordId: staged.record.recordId,
|
|
1282
|
+
description: staged.record.description,
|
|
1283
|
+
};
|
|
1284
|
+
const review = localExecEscapeReview(request, cwd, "executable" in request && request.executable === "git"
|
|
1285
|
+
? gitGrantFor(cwd, executionPath)
|
|
1286
|
+
: undefined);
|
|
1287
|
+
const presented = await localTools.presentApproval(sessionId, pending, escapeApprovals, review, review.prefix === undefined
|
|
1288
|
+
? undefined
|
|
1289
|
+
: selectRule(pending.recordId, review.prefix));
|
|
1290
|
+
if (presented.kind !== "presented")
|
|
1291
|
+
throw new Error("Local escape approval could not be presented");
|
|
1292
|
+
return pending;
|
|
1293
|
+
};
|
|
1294
|
+
const policyForSession = (sessionId) => async ({ request, cwd, mode, executionPath }) => {
|
|
1295
|
+
if (mode !== "workspace" || !("executable" in request))
|
|
1296
|
+
return undefined;
|
|
1297
|
+
let configuredRemotes;
|
|
1298
|
+
if (request.executable === "git") {
|
|
1299
|
+
configuredRemotes = gitGrantFor(cwd, executionPath)?.configuredRemotes;
|
|
1300
|
+
if (configuredRemotes === undefined)
|
|
1301
|
+
return undefined;
|
|
1302
|
+
}
|
|
1303
|
+
const matched = execRules.match([request.executable, ...request.args], configuredRemotes);
|
|
1304
|
+
if (matched === undefined)
|
|
1305
|
+
return undefined;
|
|
1306
|
+
input.onExecRuleMatch?.({
|
|
1307
|
+
agentId,
|
|
1308
|
+
executable: request.executable,
|
|
1309
|
+
ruleId: matched.ruleId,
|
|
1310
|
+
});
|
|
1311
|
+
return shellOutput.parse(await dispatchApprovedLocalExec(request, cwd, executionPath, configRoot.home, localTools.processes.scope(sessionId), mode));
|
|
1312
|
+
};
|
|
1313
|
+
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
1314
|
processes = localTools.processes;
|
|
1020
1315
|
const sessionProcesses = processes;
|
|
1021
1316
|
const modelSource = createPrivateLocalModelSource(models, selectedProviderId, selectedModelId, providerCacheKey, input.reasoning, { agent: readAgentModel });
|
|
@@ -1041,6 +1336,40 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1041
1336
|
readSettlement = host.readSettlement;
|
|
1042
1337
|
}
|
|
1043
1338
|
const sessionOwner = agent;
|
|
1339
|
+
async function recoverEscapes(sessionId) {
|
|
1340
|
+
for (const recordId of escapeLedger.pendingApplications())
|
|
1341
|
+
await escapeLedger.apply(recordId, escapeApply);
|
|
1342
|
+
for (const recordId of escapeLedger.pendingHandoffs())
|
|
1343
|
+
await deliverEscapeResult(recordId);
|
|
1344
|
+
const page = await escapeApprovals.list({ state: "pending", limit: 100 });
|
|
1345
|
+
for (const view of page.items) {
|
|
1346
|
+
if (view.record.requester.sessionId !== sessionId)
|
|
1347
|
+
continue;
|
|
1348
|
+
if (view.record.module !== "computer" ||
|
|
1349
|
+
view.record.operation !== "exec")
|
|
1350
|
+
continue;
|
|
1351
|
+
const input = escapeLedger.readInput(view.record.recordId);
|
|
1352
|
+
const evidence = z
|
|
1353
|
+
.object({
|
|
1354
|
+
request: z.object({
|
|
1355
|
+
command: z.string().optional(),
|
|
1356
|
+
executable: z.string().optional(),
|
|
1357
|
+
args: z.array(z.string()).optional(),
|
|
1358
|
+
escape: z.object({ justification: z.string() }),
|
|
1359
|
+
}),
|
|
1360
|
+
cwd: z.string(),
|
|
1361
|
+
})
|
|
1362
|
+
.parse(input);
|
|
1363
|
+
const review = localExecEscapeReview(evidence.request, evidence.cwd);
|
|
1364
|
+
await localTools.presentApproval(sessionId, {
|
|
1365
|
+
kind: "pending_approval",
|
|
1366
|
+
recordId: view.record.recordId,
|
|
1367
|
+
description: view.record.description,
|
|
1368
|
+
}, escapeApprovals, review, review.prefix === undefined
|
|
1369
|
+
? undefined
|
|
1370
|
+
: selectRule(view.record.recordId, review.prefix));
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1044
1373
|
const setAgentModel = async (input, signal) => {
|
|
1045
1374
|
const preference = parseModelPreference(input);
|
|
1046
1375
|
if (preference === null)
|
|
@@ -1053,6 +1382,10 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1053
1382
|
// Session override is cleared so the running Session follows the default.
|
|
1054
1383
|
const getSession = (sessionId, attachment) => {
|
|
1055
1384
|
const session = localTools.bindSession(sessionId, sessionOwner.session(sessionId), attachment);
|
|
1385
|
+
if (attachment !== undefined)
|
|
1386
|
+
void recoverEscapes(sessionId).catch((cause) => {
|
|
1387
|
+
logEvent("local-approval-recovery-failed", cause);
|
|
1388
|
+
});
|
|
1056
1389
|
return {
|
|
1057
1390
|
...session,
|
|
1058
1391
|
modelPreference: {
|
|
@@ -1076,11 +1409,11 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1076
1409
|
getSession,
|
|
1077
1410
|
presentApproval: localTools.presentApproval,
|
|
1078
1411
|
setModel: setAgentModel,
|
|
1079
|
-
|
|
1412
|
+
setMode: (mode) => {
|
|
1413
|
+
sandboxOwner.assertModeReady(mode);
|
|
1080
1414
|
sandboxDatabase
|
|
1081
|
-
.prepare("UPDATE private_local_identity SET
|
|
1082
|
-
.run(
|
|
1083
|
-
sandboxOwner.assertReady();
|
|
1415
|
+
.prepare("UPDATE private_local_identity SET mode = ? WHERE singleton = 1")
|
|
1416
|
+
.run(mode);
|
|
1084
1417
|
},
|
|
1085
1418
|
sandboxStatus: sandboxOwner.status,
|
|
1086
1419
|
setArchived: (archived) => {
|
|
@@ -1140,10 +1473,12 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1140
1473
|
? Promise.reject(new Error("Local settlement reads are unavailable"))
|
|
1141
1474
|
: readSettlement(sessionId, commandId),
|
|
1142
1475
|
idle: async () => {
|
|
1476
|
+
const busy = { idle: false, wakeAt: null };
|
|
1143
1477
|
if (sessionProcesses.running())
|
|
1144
|
-
return
|
|
1145
|
-
|
|
1146
|
-
|
|
1478
|
+
return busy;
|
|
1479
|
+
const settlement = await runtime.settled();
|
|
1480
|
+
if (!settlement.idle)
|
|
1481
|
+
return busy;
|
|
1147
1482
|
// Delegated work still owed to this Agent is not read here: that read
|
|
1148
1483
|
// reopens each child, and the child's reply mail reopens this Agent.
|
|
1149
1484
|
let after;
|
|
@@ -1151,7 +1486,7 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1151
1486
|
const page = await sessionOwner.listSessions(after);
|
|
1152
1487
|
for (const row of page.sessions)
|
|
1153
1488
|
if (row.state === "running" || row.state === "needs_you")
|
|
1154
|
-
return
|
|
1489
|
+
return busy;
|
|
1155
1490
|
after = page.next;
|
|
1156
1491
|
} while (after !== undefined);
|
|
1157
1492
|
// Reconciliation enqueues replies before advancing its settled cursor.
|
|
@@ -1159,8 +1494,11 @@ export async function openPrivateLocalRuntimeHost(input, suppliedModels) {
|
|
|
1159
1494
|
// while we await settlement or Session reads, especially during stop
|
|
1160
1495
|
// when delivery cannot reopen a parent. The owner rechecks admissions
|
|
1161
1496
|
// and uses after this observation before forgetting the Agent.
|
|
1162
|
-
return
|
|
1163
|
-
|
|
1497
|
+
return {
|
|
1498
|
+
idle: input.mail === undefined ||
|
|
1499
|
+
!(await hasPrivateLocalPendingMail(modelDatabase)),
|
|
1500
|
+
wakeAt: settlement.wakeAt,
|
|
1501
|
+
};
|
|
1164
1502
|
},
|
|
1165
1503
|
close: () => {
|
|
1166
1504
|
closing ??= closePrivateLocalResources({
|