@fungi.computer/truffle 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +542 -2
- package/computerd-loopback.cjs +16 -0
- package/dist/auth-cli.d.ts +46 -0
- package/dist/auth-cli.js +373 -0
- package/dist/fungi-account-command.d.ts +18 -0
- package/dist/fungi-account-command.js +95 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +23 -0
- package/dist/internal/image-fit-metafile.json +106 -0
- package/dist/internal/local-image-fit-worker.mjs +960 -0
- package/dist/internal/private-workerd-sandbox-worker.mjs +27487 -0
- package/dist/internal/sandbox-metafile.json +6726 -0
- package/dist/linear-auth.d.ts +4 -0
- package/dist/linear-auth.js +72 -0
- package/dist/local-archive-handoff.d.ts +12 -0
- package/dist/local-archive-handoff.js +21 -0
- package/dist/local-extensions.d.ts +143 -0
- package/dist/local-extensions.js +624 -0
- package/dist/local-image-fit.d.ts +30 -0
- package/dist/local-image-fit.js +105 -0
- package/dist/local-login.d.ts +55 -0
- package/dist/local-login.js +167 -0
- package/dist/local-models.d.ts +16 -0
- package/dist/local-models.js +82 -0
- package/dist/local-session-archives.d.ts +8 -0
- package/dist/local-session-archives.js +152 -0
- package/dist/local-skill-source.d.ts +21 -0
- package/dist/local-skill-source.js +433 -0
- package/dist/log-event.d.ts +9 -0
- package/dist/log-event.js +13 -0
- package/dist/private-bounded-response.d.ts +2 -0
- package/dist/private-bounded-response.js +41 -0
- package/dist/private-bun-shiitake-host.d.ts +11 -0
- package/dist/private-bun-shiitake-host.js +66 -0
- package/dist/private-file-credential-store.d.ts +58 -0
- package/dist/private-file-credential-store.js +331 -0
- package/dist/private-files-patch.d.ts +4 -0
- package/dist/private-files-patch.js +104 -0
- package/dist/private-fungi-account.d.ts +252 -0
- package/dist/private-fungi-account.js +1329 -0
- package/dist/private-fungi-auth-error.d.ts +5 -0
- package/dist/private-fungi-auth-error.js +7 -0
- package/dist/private-fungi-mcp-connection.d.ts +26 -0
- package/dist/private-fungi-mcp-connection.js +162 -0
- package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
- package/dist/private-fungi-mcp-endpoint.js +15 -0
- package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
- package/dist/private-fungi-mcp-selection-schema.js +15 -0
- package/dist/private-fungi-mcp-selection.d.ts +14 -0
- package/dist/private-fungi-mcp-selection.js +49 -0
- package/dist/private-fungi-model-api.d.ts +6 -0
- package/dist/private-fungi-model-api.js +125 -0
- package/dist/private-fungi-model-catalog.d.ts +16 -0
- package/dist/private-fungi-model-catalog.js +86 -0
- package/dist/private-fungi-model-provider.d.ts +6 -0
- package/dist/private-fungi-model-provider.js +51 -0
- package/dist/private-fungi-pending-logouts.d.ts +34 -0
- package/dist/private-fungi-pending-logouts.js +135 -0
- package/dist/private-fungi-service-logout.d.ts +44 -0
- package/dist/private-fungi-service-logout.js +59 -0
- package/dist/private-invocation-computer.d.ts +58 -0
- package/dist/private-invocation-computer.js +498 -0
- package/dist/private-invocation-files.d.ts +30 -0
- package/dist/private-invocation-files.js +520 -0
- package/dist/private-local-acp-stdio.d.ts +17 -0
- package/dist/private-local-acp-stdio.js +145 -0
- package/dist/private-local-admission-gate.d.ts +14 -0
- package/dist/private-local-admission-gate.js +52 -0
- package/dist/private-local-agent-control.d.ts +101 -0
- package/dist/private-local-agent-control.js +30 -0
- package/dist/private-local-agent-default-model.d.ts +12 -0
- package/dist/private-local-agent-default-model.js +31 -0
- package/dist/private-local-auth-rpc.d.ts +129 -0
- package/dist/private-local-auth-rpc.js +396 -0
- package/dist/private-local-bwrap.d.ts +13 -0
- package/dist/private-local-bwrap.js +70 -0
- package/dist/private-local-client-version.d.ts +5 -0
- package/dist/private-local-client-version.js +43 -0
- package/dist/private-local-computer-attach.d.ts +49 -0
- package/dist/private-local-computer-attach.js +230 -0
- package/dist/private-local-computer-backend.d.ts +282 -0
- package/dist/private-local-computer-backend.js +402 -0
- package/dist/private-local-computer-processes.d.ts +202 -0
- package/dist/private-local-computer-processes.js +613 -0
- package/dist/private-local-config-root.d.ts +21 -0
- package/dist/private-local-config-root.js +19 -0
- package/dist/private-local-containment.d.ts +51 -0
- package/dist/private-local-containment.js +161 -0
- package/dist/private-local-default-model.d.ts +11 -0
- package/dist/private-local-default-model.js +45 -0
- package/dist/private-local-delegation.d.ts +36 -0
- package/dist/private-local-delegation.js +133 -0
- package/dist/private-local-effective-model.d.ts +27 -0
- package/dist/private-local-effective-model.js +17 -0
- package/dist/private-local-exec-environment.d.ts +16 -0
- package/dist/private-local-exec-environment.js +11 -0
- package/dist/private-local-exec-launch.d.ts +41 -0
- package/dist/private-local-exec-launch.js +51 -0
- package/dist/private-local-exec-path.d.ts +6 -0
- package/dist/private-local-exec-path.js +40 -0
- package/dist/private-local-git-exclude.d.ts +2 -0
- package/dist/private-local-git-exclude.js +33 -0
- package/dist/private-local-http-dispatch.d.ts +5 -0
- package/dist/private-local-http-dispatch.js +14 -0
- package/dist/private-local-linux-sandbox.d.ts +8 -0
- package/dist/private-local-linux-sandbox.js +67 -0
- package/dist/private-local-mail.d.ts +13 -0
- package/dist/private-local-mail.js +77 -0
- package/dist/private-local-mesh-enrollment.d.ts +34 -0
- package/dist/private-local-mesh-enrollment.js +188 -0
- package/dist/private-local-mesh-links.d.ts +21 -0
- package/dist/private-local-mesh-links.js +229 -0
- package/dist/private-local-mesh-record.d.ts +120 -0
- package/dist/private-local-mesh-record.js +55 -0
- package/dist/private-local-process-identity.d.ts +20 -0
- package/dist/private-local-process-identity.js +40 -0
- package/dist/private-local-runtime-host.d.ts +192 -0
- package/dist/private-local-runtime-host.js +1177 -0
- package/dist/private-local-sandbox-policy.d.ts +21 -0
- package/dist/private-local-sandbox-policy.js +121 -0
- package/dist/private-local-sandbox.d.ts +37 -0
- package/dist/private-local-sandbox.js +114 -0
- package/dist/private-local-seatbelt.d.ts +24 -0
- package/dist/private-local-seatbelt.js +68 -0
- package/dist/private-local-service-contract.d.ts +170 -0
- package/dist/private-local-service-contract.js +1 -0
- package/dist/private-local-service-discovery.d.ts +217 -0
- package/dist/private-local-service-discovery.js +322 -0
- package/dist/private-local-service-ids.d.ts +3 -0
- package/dist/private-local-service-ids.js +8 -0
- package/dist/private-local-service-lease.d.ts +16 -0
- package/dist/private-local-service-lease.js +105 -0
- package/dist/private-local-service-listener.d.ts +22 -0
- package/dist/private-local-service-listener.js +407 -0
- package/dist/private-local-service-log.d.ts +128 -0
- package/dist/private-local-service-log.js +321 -0
- package/dist/private-local-service-owner.d.ts +135 -0
- package/dist/private-local-service-owner.js +1369 -0
- package/dist/private-local-service-process.d.ts +1 -0
- package/dist/private-local-service-process.js +8 -0
- package/dist/private-local-service-termination.d.ts +7 -0
- package/dist/private-local-service-termination.js +67 -0
- package/dist/private-local-service.d.ts +46 -0
- package/dist/private-local-service.js +606 -0
- package/dist/private-local-session-discovery.d.ts +66 -0
- package/dist/private-local-session-discovery.js +278 -0
- package/dist/private-local-shiitake-host.d.ts +44 -0
- package/dist/private-local-shiitake-host.js +143 -0
- package/dist/private-local-startup-model.d.ts +51 -0
- package/dist/private-local-startup-model.js +86 -0
- package/dist/private-local-stop-all.d.ts +27 -0
- package/dist/private-local-stop-all.js +26 -0
- package/dist/private-local-ui-attachment.d.ts +19 -0
- package/dist/private-local-ui-attachment.js +71 -0
- package/dist/private-local-worker-server.d.ts +16 -0
- package/dist/private-local-worker-server.js +144 -0
- package/dist/private-mcp-form-contract.d.ts +186 -0
- package/dist/private-mcp-form-contract.js +53 -0
- package/dist/private-mcp-form-elicitation.d.ts +18 -0
- package/dist/private-mcp-form-elicitation.js +97 -0
- package/dist/private-mcp-form-session.d.ts +172 -0
- package/dist/private-mcp-form-session.js +148 -0
- package/dist/private-openai-claims.d.ts +30 -0
- package/dist/private-openai-claims.js +69 -0
- package/dist/private-oyster-mcp-auth.d.ts +13 -0
- package/dist/private-oyster-mcp-auth.js +57 -0
- package/dist/private-provider-rejection.d.ts +15 -0
- package/dist/private-provider-rejection.js +24 -0
- package/dist/private-sqlite-owner.d.ts +39 -0
- package/dist/private-sqlite-owner.js +169 -0
- package/dist/private-team-computers.d.ts +38 -0
- package/dist/private-team-computers.js +110 -0
- package/dist/private-workerd-sandbox.d.ts +3 -0
- package/dist/private-workerd-sandbox.js +347 -0
- package/dist/seatbelt/safe-base.sbpl +79 -0
- package/dist/subscription-activation.d.ts +9 -0
- package/dist/subscription-activation.js +62 -0
- package/dist/subscription-identity.d.ts +23 -0
- package/dist/subscription-identity.js +124 -0
- package/dist/subscription-turn.d.ts +17 -0
- package/dist/subscription-turn.js +143 -0
- package/dist/team-apps.d.ts +24 -0
- package/dist/team-apps.js +128 -0
- package/dist/tracker-git-credential.d.ts +15 -0
- package/dist/tracker-git-credential.js +87 -0
- package/native/bin/LICENSE.bubblewrap +502 -0
- package/native/bin/LICENSE.libcap +112 -0
- package/native/bin/LICENSE.sandbox-runtime +201 -0
- package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
- package/native/bin/build.json +4037 -0
- package/native/bin/bwrap-build-source/Dockerfile +48 -0
- package/native/bin/bwrap-build-source/README.md +96 -0
- package/native/bin/bwrap-build-source/build.mjs +52 -0
- package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
- package/native/bin/bwrap-build-source/pin.mjs +21 -0
- package/native/bin/bwrap-build-source/record.mjs +74 -0
- package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
- package/native/bin/bwrap-build-source/vendor/README.md +23 -0
- package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
- package/native/bin/bwrap-build.json +44 -0
- package/native/bin/fungi-apply-patch +0 -0
- package/native/bin/fungi-bwrap +0 -0
- package/native/bin/fungi-bwrap.sha256 +1 -0
- package/native/bin/fungi-seccomp.bpf +0 -0
- package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
- package/package.json +161 -3
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
import { spawn, } from "node:child_process";
|
|
2
|
+
import { closeSync, openSync, readSync, writeSync } from "node:fs";
|
|
3
|
+
import { mkdir, rm } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
7
|
+
import { defineIdFamily } from "@fungi.computer/cairn";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { combinedArgvBytes, launchCwdSchema, launchStdinSchema, maximumLaunchTextLength, } from "./private-local-exec-launch.js";
|
|
10
|
+
import { privateLocalExecEnvironment } from "./private-local-exec-environment.js";
|
|
11
|
+
import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
|
|
12
|
+
import { Writable } from "node:stream";
|
|
13
|
+
/**
|
|
14
|
+
* Background processes of the local Computer: the one owner of "a command that
|
|
15
|
+
* outlives a single exec call".
|
|
16
|
+
*
|
|
17
|
+
* `exec` kills its whole process group when its call returns, so `nohup … &`
|
|
18
|
+
* cannot survive it. A process started here instead belongs to one scope (a
|
|
19
|
+
* Session, or the invocation when the host has no Sessions) of one host. It
|
|
20
|
+
* runs in its own process group with stdout and stderr kept in one bounded
|
|
21
|
+
* circular log under the project's `.fungi/processes/`, and it ends when it exits, when
|
|
22
|
+
* `kill` is called, when its scope is disposed (Session deleted), or when the
|
|
23
|
+
* host closes. The shell/exec surface owns start, poll, wait, list and kill.
|
|
24
|
+
*/
|
|
25
|
+
/** Running processes one scope (Session) may hold at once. */
|
|
26
|
+
const maximumRunningProcessesPerScope = 8;
|
|
27
|
+
/** Running processes one host may hold at once; mirrors the container Computer. */
|
|
28
|
+
const maximumRunningProcesses = 16;
|
|
29
|
+
/** Records, running or exited, one scope keeps; the oldest exited is evicted. */
|
|
30
|
+
const maximumProcessRecordsPerScope = 32;
|
|
31
|
+
/** Bytes of combined stdout and stderr one process log keeps. */
|
|
32
|
+
const maximumLogBytes = 8 * 1_024 * 1_024;
|
|
33
|
+
/** Bytes one read returns at most. */
|
|
34
|
+
const maximumReadBytes = 64 * 1_024;
|
|
35
|
+
const defaultReadBytes = 16 * 1_024;
|
|
36
|
+
/** One wait returns after at most this long; call it again to keep waiting. */
|
|
37
|
+
const maximumWaitMs = 60_000;
|
|
38
|
+
const defaultWaitMs = 10_000;
|
|
39
|
+
/** SIGTERM grace before SIGKILL when a process group is stopped. */
|
|
40
|
+
const terminationGraceMs = 2_000;
|
|
41
|
+
const localProcessIds = defineIdFamily({
|
|
42
|
+
family: "local-computer-process",
|
|
43
|
+
version: 1,
|
|
44
|
+
kinds: { process: { prefix: "local_process" } },
|
|
45
|
+
});
|
|
46
|
+
const processIdSchema = z
|
|
47
|
+
.string()
|
|
48
|
+
.max(128)
|
|
49
|
+
.refine((id) => localProcessIds.process.is(id), "Unknown process id");
|
|
50
|
+
const processStateSchema = z.enum(["running", "exited"]);
|
|
51
|
+
const processSchema = z.strictObject({
|
|
52
|
+
id: processIdSchema,
|
|
53
|
+
command: z.string(),
|
|
54
|
+
cwd: z.string(),
|
|
55
|
+
state: processStateSchema,
|
|
56
|
+
exitCode: z.number().int().nullable(),
|
|
57
|
+
signal: z.string().nullable(),
|
|
58
|
+
startedAt: z.number().int(),
|
|
59
|
+
endedAt: z.number().int().nullable(),
|
|
60
|
+
logPath: z.string(),
|
|
61
|
+
logBytes: z.number().int().nonnegative(),
|
|
62
|
+
logStartOffset: z.number().int().nonnegative(),
|
|
63
|
+
logTruncated: z.boolean(),
|
|
64
|
+
});
|
|
65
|
+
/** The intentional shell form of background `start`. */
|
|
66
|
+
const shellStartInput = z.strictObject({
|
|
67
|
+
operation: z.literal("start"),
|
|
68
|
+
command: z.string().min(1).max(maximumLaunchTextLength),
|
|
69
|
+
stdin: launchStdinSchema.optional(),
|
|
70
|
+
cwd: launchCwdSchema.optional(),
|
|
71
|
+
});
|
|
72
|
+
/** The direct form of background `start`: argv run with no shell interpretation. */
|
|
73
|
+
const directStartInput = z
|
|
74
|
+
.strictObject({
|
|
75
|
+
operation: z.literal("start"),
|
|
76
|
+
executable: z.string().min(1),
|
|
77
|
+
args: z.array(z.string()).default([]),
|
|
78
|
+
stdin: launchStdinSchema.optional(),
|
|
79
|
+
cwd: launchCwdSchema.optional(),
|
|
80
|
+
})
|
|
81
|
+
.refine((value) => combinedArgvBytes(value.executable, value.args) <=
|
|
82
|
+
maximumLaunchTextLength, {
|
|
83
|
+
message: `Computer exec executable and args exceed ${String(maximumLaunchTextLength)} combined UTF-8 bytes`,
|
|
84
|
+
path: ["args"],
|
|
85
|
+
});
|
|
86
|
+
const idInput = z.strictObject({ id: processIdSchema });
|
|
87
|
+
const readInput = z.strictObject({
|
|
88
|
+
id: processIdSchema,
|
|
89
|
+
offset: z.number().int().nonnegative().default(0),
|
|
90
|
+
maxBytes: z
|
|
91
|
+
.number()
|
|
92
|
+
.int()
|
|
93
|
+
.min(1)
|
|
94
|
+
.max(maximumReadBytes)
|
|
95
|
+
.default(defaultReadBytes),
|
|
96
|
+
});
|
|
97
|
+
const waitInput = z.strictObject({
|
|
98
|
+
id: processIdSchema,
|
|
99
|
+
timeoutMs: z.number().int().min(1).max(maximumWaitMs).default(defaultWaitMs),
|
|
100
|
+
});
|
|
101
|
+
const listOutput = z.strictObject({
|
|
102
|
+
processes: z.array(processSchema).max(maximumProcessRecordsPerScope),
|
|
103
|
+
});
|
|
104
|
+
const readOutput = z.strictObject({
|
|
105
|
+
process: processSchema,
|
|
106
|
+
offset: z.number().int().nonnegative(),
|
|
107
|
+
nextOffset: z.number().int().nonnegative(),
|
|
108
|
+
text: z.string(),
|
|
109
|
+
complete: z.boolean(),
|
|
110
|
+
});
|
|
111
|
+
const waitOutput = z.strictObject({
|
|
112
|
+
process: processSchema,
|
|
113
|
+
timedOut: z.boolean(),
|
|
114
|
+
});
|
|
115
|
+
const killOutput = z.strictObject({
|
|
116
|
+
process: processSchema,
|
|
117
|
+
signalled: z.boolean(),
|
|
118
|
+
});
|
|
119
|
+
/** No process with this id was started in this scope. */
|
|
120
|
+
class PrivateLocalProcessNotFoundError extends Error {
|
|
121
|
+
constructor(id) {
|
|
122
|
+
super(`Computer process not found: ${id}`);
|
|
123
|
+
this.name = "PrivateLocalProcessNotFoundError";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/** Every process slot of this scope or host is occupied. */
|
|
127
|
+
class PrivateLocalProcessLimitError extends Error {
|
|
128
|
+
constructor(limit, owner) {
|
|
129
|
+
super(`This ${owner} can run at most ${String(limit)} background processes at once; wait for or kill one with exec first`);
|
|
130
|
+
this.name = "PrivateLocalProcessLimitError";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Signal a whole process group; false when no member could be signalled. */
|
|
134
|
+
function signalGroup(groupId, signal) {
|
|
135
|
+
try {
|
|
136
|
+
process.kill(-groupId, signal);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error instanceof Error &&
|
|
141
|
+
"code" in error &&
|
|
142
|
+
(error.code === "ESRCH" || error.code === "EPERM"))
|
|
143
|
+
return false;
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function groupAlive(groupId) {
|
|
148
|
+
try {
|
|
149
|
+
process.kill(-groupId, 0);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
catch (cause) {
|
|
153
|
+
if (cause instanceof Error && "code" in cause) {
|
|
154
|
+
if (cause.code === "ESRCH")
|
|
155
|
+
return false;
|
|
156
|
+
if (cause.code === "EPERM")
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
throw cause;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function toCommandSpec(request) {
|
|
163
|
+
if ("command" in request) {
|
|
164
|
+
const spec = { kind: "shell", command: request.command };
|
|
165
|
+
return request.stdin === undefined
|
|
166
|
+
? spec
|
|
167
|
+
: { ...spec, stdin: request.stdin };
|
|
168
|
+
}
|
|
169
|
+
const spec = {
|
|
170
|
+
kind: "direct",
|
|
171
|
+
executable: request.executable,
|
|
172
|
+
args: request.args,
|
|
173
|
+
};
|
|
174
|
+
return request.stdin === undefined ? spec : { ...spec, stdin: request.stdin };
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* A one-line, human-readable label for a spec; never used for execution.
|
|
178
|
+
* Each direct-form arg is JSON-quoted so `list`/`poll` output does not read
|
|
179
|
+
* like a re-runnable shell line.
|
|
180
|
+
*/
|
|
181
|
+
function describeCommandSpec(spec) {
|
|
182
|
+
return spec.kind === "shell"
|
|
183
|
+
? spec.command
|
|
184
|
+
: [spec.executable, ...spec.args.map((arg) => JSON.stringify(arg))].join(" ");
|
|
185
|
+
}
|
|
186
|
+
function isErrnoException(error) {
|
|
187
|
+
return "code" in error;
|
|
188
|
+
}
|
|
189
|
+
/** Drop a trailing partial UTF-8 sequence so a later read resumes on it. */
|
|
190
|
+
function completeUtf8Length(bytes, length) {
|
|
191
|
+
for (let back = 1; back <= Math.min(4, length); back += 1) {
|
|
192
|
+
const byte = bytes[length - back] ?? 0;
|
|
193
|
+
if ((byte & 0xc0) === 0x80)
|
|
194
|
+
continue;
|
|
195
|
+
if (byte < 0xc2 || byte > 0xf4)
|
|
196
|
+
return length;
|
|
197
|
+
const expected = byte >= 0xf0 ? 4 : byte >= 0xe0 ? 3 : 2;
|
|
198
|
+
return back < expected ? length - back : length;
|
|
199
|
+
}
|
|
200
|
+
return length;
|
|
201
|
+
}
|
|
202
|
+
class ManagedProcess {
|
|
203
|
+
spec;
|
|
204
|
+
cwd;
|
|
205
|
+
id = localProcessIds.process.generate();
|
|
206
|
+
startedAt = Date.now();
|
|
207
|
+
logPath;
|
|
208
|
+
closed;
|
|
209
|
+
#child;
|
|
210
|
+
#groupId;
|
|
211
|
+
#fd;
|
|
212
|
+
#logBytes = 0;
|
|
213
|
+
#logTruncated = false;
|
|
214
|
+
#exitCode = null;
|
|
215
|
+
#signal = null;
|
|
216
|
+
#endedAt = null;
|
|
217
|
+
#leaderExited = false;
|
|
218
|
+
command;
|
|
219
|
+
constructor(spec, cwd, logDirectory, executionPath, home, launch) {
|
|
220
|
+
this.spec = spec;
|
|
221
|
+
this.cwd = cwd;
|
|
222
|
+
this.command = describeCommandSpec(spec);
|
|
223
|
+
this.logPath = join(logDirectory, `${this.id}.log`);
|
|
224
|
+
this.#fd = openSync(this.logPath, "wx+", 0o600);
|
|
225
|
+
const closed = Promise.withResolvers();
|
|
226
|
+
this.closed = closed.promise;
|
|
227
|
+
const env = privateLocalExecEnvironment(executionPath, {
|
|
228
|
+
HOME: home,
|
|
229
|
+
XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
|
|
230
|
+
DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS,
|
|
231
|
+
});
|
|
232
|
+
if (launch !== undefined) {
|
|
233
|
+
delete env.XDG_RUNTIME_DIR;
|
|
234
|
+
delete env.DBUS_SESSION_BUS_ADDRESS;
|
|
235
|
+
Object.assign(env, launch.environment);
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
// Its own process group, so neither exec's finalizer nor this host's
|
|
239
|
+
// terminal signals reach it; kill() owns ending the whole group.
|
|
240
|
+
const argv = spec.kind === "shell"
|
|
241
|
+
? ["sh", "-c", spec.command]
|
|
242
|
+
: [spec.executable, ...spec.args];
|
|
243
|
+
const stdio = [
|
|
244
|
+
spec.stdin === undefined ? "ignore" : "pipe",
|
|
245
|
+
"pipe",
|
|
246
|
+
"pipe",
|
|
247
|
+
];
|
|
248
|
+
if (launch?.seccompFilter !== undefined)
|
|
249
|
+
stdio.push("pipe");
|
|
250
|
+
this.#child = spawn(launch?.executable ?? argv[0], launch?.args ?? argv.slice(1), {
|
|
251
|
+
cwd,
|
|
252
|
+
env,
|
|
253
|
+
detached: true,
|
|
254
|
+
stdio,
|
|
255
|
+
});
|
|
256
|
+
if (launch?.seccompFilter !== undefined) {
|
|
257
|
+
const filter = this.#child.stdio[3];
|
|
258
|
+
if (!(filter instanceof Writable)) {
|
|
259
|
+
this.#child.kill("SIGKILL");
|
|
260
|
+
throw new Error("Local sandbox filter input fd3 is unavailable");
|
|
261
|
+
}
|
|
262
|
+
// A failed filter delivery cannot become a bare retry or an unhandled
|
|
263
|
+
// stream error. The existing process owner records and reaps failure.
|
|
264
|
+
filter.on("error", (error) => {
|
|
265
|
+
this.#append(Buffer.from(`\n[fungi: sandbox filter failed: ${error.message}]\n`));
|
|
266
|
+
this.killNow();
|
|
267
|
+
});
|
|
268
|
+
filter.end(launch.seccompFilter);
|
|
269
|
+
}
|
|
270
|
+
// Finite stdin, delivered once and closed; never an interactive
|
|
271
|
+
// protocol. `end()` writes the bytes and closes the write side. A
|
|
272
|
+
// child that exits or closes its own stdin before the payload drains
|
|
273
|
+
// (`true`, `head -c1`, an early exit) makes that write EPIPE; without
|
|
274
|
+
// this listener the stream emits 'error' with no handler and crashes
|
|
275
|
+
// the host process. There is no caller to reject here (`start` has
|
|
276
|
+
// already returned a process id), so this only records what happened.
|
|
277
|
+
if (spec.stdin !== undefined) {
|
|
278
|
+
const stdin = spec.stdin;
|
|
279
|
+
this.#child.stdin?.on("error", (error) => {
|
|
280
|
+
const code = isErrnoException(error) ? error.code : undefined;
|
|
281
|
+
this.#append(Buffer.from(code === "EPIPE"
|
|
282
|
+
? "\n[fungi: stdin not fully consumed]\n"
|
|
283
|
+
: `\n[fungi: stdin failed: ${error.message}]\n`));
|
|
284
|
+
});
|
|
285
|
+
this.#child.stdin?.end(Buffer.from(stdin, "utf8"));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
catch (cause) {
|
|
289
|
+
closeSync(this.#fd);
|
|
290
|
+
throw cause;
|
|
291
|
+
}
|
|
292
|
+
const pid = this.#child.pid;
|
|
293
|
+
this.#groupId =
|
|
294
|
+
pid !== undefined && Number.isSafeInteger(pid) && pid > 1
|
|
295
|
+
? pid
|
|
296
|
+
: undefined;
|
|
297
|
+
const append = (chunk) => {
|
|
298
|
+
this.#append(chunk);
|
|
299
|
+
};
|
|
300
|
+
this.#child.stdout?.on("data", append);
|
|
301
|
+
this.#child.stderr?.on("data", append);
|
|
302
|
+
this.#child.on("error", (error) => {
|
|
303
|
+
this.#append(Buffer.from(`\n[fungi: process failed: ${error.message}]\n`));
|
|
304
|
+
this.#finish(closed.resolve);
|
|
305
|
+
});
|
|
306
|
+
this.#child.on("exit", (code, signal) => {
|
|
307
|
+
this.#leaderExited = true;
|
|
308
|
+
this.#exitCode = code;
|
|
309
|
+
this.#signal = signal;
|
|
310
|
+
// The command is finished; members it left in its group are not owned
|
|
311
|
+
// by anyone else, so end them and release the log pipes.
|
|
312
|
+
void this.#terminateGroup().then(() => {
|
|
313
|
+
this.#child.stdout?.destroy();
|
|
314
|
+
this.#child.stderr?.destroy();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
this.#child.on("close", () => {
|
|
318
|
+
this.#finish(closed.resolve);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
get running() {
|
|
322
|
+
return this.#endedAt === null;
|
|
323
|
+
}
|
|
324
|
+
#append(chunk) {
|
|
325
|
+
const fd = this.#fd;
|
|
326
|
+
if (fd === undefined)
|
|
327
|
+
return;
|
|
328
|
+
// Circular storage retains the latest bytes without growing the log or
|
|
329
|
+
// copying megabytes on every append. Offsets count all produced bytes.
|
|
330
|
+
const skipped = Math.max(0, chunk.byteLength - maximumLogBytes);
|
|
331
|
+
const kept = chunk.subarray(skipped);
|
|
332
|
+
const position = (this.#logBytes + skipped) % maximumLogBytes;
|
|
333
|
+
const first = Math.min(kept.byteLength, maximumLogBytes - position);
|
|
334
|
+
writeSync(fd, kept, 0, first, position);
|
|
335
|
+
if (first < kept.byteLength)
|
|
336
|
+
writeSync(fd, kept, first, kept.byteLength - first, 0);
|
|
337
|
+
this.#logBytes += chunk.byteLength;
|
|
338
|
+
this.#logTruncated = this.#logBytes > maximumLogBytes;
|
|
339
|
+
}
|
|
340
|
+
/** Read atomically with appends, including a wrap in circular storage. */
|
|
341
|
+
read(offset, maxBytes) {
|
|
342
|
+
const end = this.#logBytes;
|
|
343
|
+
let from = Math.max(Math.max(0, end - maximumLogBytes), Math.min(offset, end));
|
|
344
|
+
const length = Math.min(maxBytes, end - from);
|
|
345
|
+
const bytes = new Uint8Array(length);
|
|
346
|
+
const fd = this.#fd ?? openSync(this.logPath, "r");
|
|
347
|
+
try {
|
|
348
|
+
const position = from % maximumLogBytes;
|
|
349
|
+
const first = Math.min(length, maximumLogBytes - position);
|
|
350
|
+
if (first > 0)
|
|
351
|
+
readSync(fd, bytes, 0, first, position);
|
|
352
|
+
if (first < length)
|
|
353
|
+
readSync(fd, bytes, first, length - first, 0);
|
|
354
|
+
}
|
|
355
|
+
finally {
|
|
356
|
+
if (this.#fd === undefined)
|
|
357
|
+
closeSync(fd);
|
|
358
|
+
}
|
|
359
|
+
// A tail or evicted cursor may start inside a UTF-8 sequence.
|
|
360
|
+
let skipped = 0;
|
|
361
|
+
while (skipped < length && ((bytes[skipped] ?? 0) & 0xc0) === 0x80)
|
|
362
|
+
skipped += 1;
|
|
363
|
+
from += skipped;
|
|
364
|
+
const content = bytes.subarray(skipped);
|
|
365
|
+
const snapshot = this.snapshot();
|
|
366
|
+
// At the final byte of an exited command no continuation can arrive;
|
|
367
|
+
// flush malformed/incomplete terminal bytes through the decoder instead.
|
|
368
|
+
const kept = snapshot.state === "exited" && from + content.length === end
|
|
369
|
+
? content.length
|
|
370
|
+
: completeUtf8Length(content, content.length);
|
|
371
|
+
return readOutput.parse({
|
|
372
|
+
process: snapshot,
|
|
373
|
+
offset: from,
|
|
374
|
+
nextOffset: from + kept,
|
|
375
|
+
text: new TextDecoder().decode(content.subarray(0, kept)),
|
|
376
|
+
complete: snapshot.state === "exited" && from + kept >= end,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
#finish(resolve) {
|
|
380
|
+
if (this.#endedAt !== null)
|
|
381
|
+
return;
|
|
382
|
+
this.#endedAt = Date.now();
|
|
383
|
+
if (this.#fd !== undefined)
|
|
384
|
+
closeSync(this.#fd);
|
|
385
|
+
this.#fd = undefined;
|
|
386
|
+
resolve(undefined);
|
|
387
|
+
}
|
|
388
|
+
/** TERM the whole group, then KILL whatever ignores it past the grace. */
|
|
389
|
+
async #terminateGroup() {
|
|
390
|
+
const groupId = this.#groupId;
|
|
391
|
+
if (groupId === undefined || !signalGroup(groupId, "SIGTERM"))
|
|
392
|
+
return false;
|
|
393
|
+
const deadline = Date.now() + terminationGraceMs;
|
|
394
|
+
while (groupAlive(groupId) && Date.now() < deadline)
|
|
395
|
+
await delay(20);
|
|
396
|
+
if (groupAlive(groupId))
|
|
397
|
+
signalGroup(groupId, "SIGKILL");
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
/** Stop this process and every member of its group; settles once it ended. */
|
|
401
|
+
async kill() {
|
|
402
|
+
if (!this.running)
|
|
403
|
+
return false;
|
|
404
|
+
const signalled = await this.#terminateGroup();
|
|
405
|
+
// A group that escaped signalling still releases our pipes and log.
|
|
406
|
+
if (this.#leaderExited || !signalled) {
|
|
407
|
+
this.#child.stdout?.destroy();
|
|
408
|
+
this.#child.stderr?.destroy();
|
|
409
|
+
}
|
|
410
|
+
await this.closed;
|
|
411
|
+
return signalled;
|
|
412
|
+
}
|
|
413
|
+
/** Synchronous last resort while the host process itself exits. */
|
|
414
|
+
killNow() {
|
|
415
|
+
if (this.running && this.#groupId !== undefined)
|
|
416
|
+
signalGroup(this.#groupId, "SIGKILL");
|
|
417
|
+
}
|
|
418
|
+
snapshot() {
|
|
419
|
+
return processSchema.parse({
|
|
420
|
+
id: this.id,
|
|
421
|
+
command: this.command,
|
|
422
|
+
cwd: this.cwd,
|
|
423
|
+
state: this.running ? "running" : "exited",
|
|
424
|
+
exitCode: this.running ? null : this.#exitCode,
|
|
425
|
+
signal: this.running ? null : this.#signal,
|
|
426
|
+
startedAt: this.startedAt,
|
|
427
|
+
endedAt: this.#endedAt,
|
|
428
|
+
logPath: this.logPath,
|
|
429
|
+
logBytes: this.#logBytes,
|
|
430
|
+
logStartOffset: Math.max(0, this.#logBytes - maximumLogBytes),
|
|
431
|
+
logTruncated: this.#logTruncated,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
/** Open the background-process owner for one invocation root. */
|
|
436
|
+
export function createPrivateLocalComputerProcesses(root, home = capturePrivateLocalConfigRoot().home) {
|
|
437
|
+
const logDirectory = join(root, ".fungi", "processes");
|
|
438
|
+
const scopes = new Map();
|
|
439
|
+
let closed = false;
|
|
440
|
+
let exitHook = false;
|
|
441
|
+
const all = () => [...scopes.values()].flatMap((owned) => [...owned.values()]);
|
|
442
|
+
const killAllNow = () => {
|
|
443
|
+
for (const owned of all())
|
|
444
|
+
owned.killNow();
|
|
445
|
+
};
|
|
446
|
+
const dispose = async (owned) => {
|
|
447
|
+
const records = [...owned.values()];
|
|
448
|
+
owned.clear();
|
|
449
|
+
await Promise.all(records.map((record) => record.kill()));
|
|
450
|
+
await Promise.all(records.map((record) => rm(record.logPath, { force: true })));
|
|
451
|
+
};
|
|
452
|
+
const scope = (key) => {
|
|
453
|
+
const owned = () => {
|
|
454
|
+
let selected = scopes.get(key);
|
|
455
|
+
if (selected === undefined) {
|
|
456
|
+
selected = new Map();
|
|
457
|
+
scopes.set(key, selected);
|
|
458
|
+
}
|
|
459
|
+
return selected;
|
|
460
|
+
};
|
|
461
|
+
const find = (id) => {
|
|
462
|
+
const record = scopes.get(key)?.get(id);
|
|
463
|
+
if (record === undefined)
|
|
464
|
+
throw new PrivateLocalProcessNotFoundError(id);
|
|
465
|
+
return record;
|
|
466
|
+
};
|
|
467
|
+
return Object.freeze({
|
|
468
|
+
start: async (spec, cwd, executionPath, sandbox) => {
|
|
469
|
+
if (closed)
|
|
470
|
+
throw new Error("Local Computer processes are closed");
|
|
471
|
+
const records = owned();
|
|
472
|
+
const running = [...records.values()].filter((r) => r.running).length;
|
|
473
|
+
if (running >= maximumRunningProcessesPerScope)
|
|
474
|
+
throw new PrivateLocalProcessLimitError(maximumRunningProcessesPerScope, "Session");
|
|
475
|
+
if (all().filter((r) => r.running).length >= maximumRunningProcesses)
|
|
476
|
+
throw new PrivateLocalProcessLimitError(maximumRunningProcesses, "host");
|
|
477
|
+
if (records.size >= maximumProcessRecordsPerScope) {
|
|
478
|
+
const oldest = [...records.values()].find((r) => !r.running);
|
|
479
|
+
if (oldest !== undefined) {
|
|
480
|
+
records.delete(oldest.id);
|
|
481
|
+
await rm(oldest.logPath, { force: true });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
await mkdir(logDirectory, { recursive: true, mode: 0o700 });
|
|
485
|
+
if (!exitHook) {
|
|
486
|
+
// A host that exits without close still takes its process groups.
|
|
487
|
+
process.on("exit", killAllNow);
|
|
488
|
+
exitHook = true;
|
|
489
|
+
}
|
|
490
|
+
const argv = spec.kind === "shell"
|
|
491
|
+
? ["sh", "-c", spec.command]
|
|
492
|
+
: [spec.executable, ...spec.args];
|
|
493
|
+
const launch = sandbox?.(cwd, argv);
|
|
494
|
+
const started = new ManagedProcess(spec, cwd, logDirectory, executionPath, home, launch);
|
|
495
|
+
records.set(started.id, started);
|
|
496
|
+
return started.snapshot();
|
|
497
|
+
},
|
|
498
|
+
status: (id) => find(id).snapshot(),
|
|
499
|
+
list: () => [...(scopes.get(key)?.values() ?? [])].map((r) => r.snapshot()),
|
|
500
|
+
read: (id, offset, maxBytes) => Promise.resolve(find(id).read(offset, maxBytes)),
|
|
501
|
+
wait: async (id, timeoutMs, signal) => {
|
|
502
|
+
const record = find(id);
|
|
503
|
+
const controller = new AbortController();
|
|
504
|
+
const abandon = () => {
|
|
505
|
+
controller.abort();
|
|
506
|
+
};
|
|
507
|
+
signal.addEventListener("abort", abandon, { once: true });
|
|
508
|
+
try {
|
|
509
|
+
const timedOut = await Promise.race([
|
|
510
|
+
record.closed.then(() => false),
|
|
511
|
+
delay(timeoutMs, true, { signal: controller.signal }),
|
|
512
|
+
]);
|
|
513
|
+
return waitOutput.parse({ process: record.snapshot(), timedOut });
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
signal.removeEventListener("abort", abandon);
|
|
517
|
+
controller.abort();
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
kill: async (id) => {
|
|
521
|
+
const record = find(id);
|
|
522
|
+
const signalled = await record.kill();
|
|
523
|
+
return killOutput.parse({ process: record.snapshot(), signalled });
|
|
524
|
+
},
|
|
525
|
+
});
|
|
526
|
+
};
|
|
527
|
+
return Object.freeze({
|
|
528
|
+
scope,
|
|
529
|
+
disposeScope: async (key) => {
|
|
530
|
+
const owned = scopes.get(key);
|
|
531
|
+
scopes.delete(key);
|
|
532
|
+
if (owned !== undefined)
|
|
533
|
+
await dispose(owned);
|
|
534
|
+
},
|
|
535
|
+
running: () => all().some((record) => record.running),
|
|
536
|
+
close: async () => {
|
|
537
|
+
closed = true;
|
|
538
|
+
const owned = [...scopes.values()];
|
|
539
|
+
scopes.clear();
|
|
540
|
+
try {
|
|
541
|
+
await Promise.all(owned.map(dispose));
|
|
542
|
+
}
|
|
543
|
+
finally {
|
|
544
|
+
process.off("exit", killAllNow);
|
|
545
|
+
exitHook = false;
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Background operations share the Computer's one shell/exec surface; a
|
|
552
|
+
* plain (not discriminated) union, since `start`'s shell and direct branches
|
|
553
|
+
* share the `operation:"start"` literal that `discriminatedUnion` requires
|
|
554
|
+
* to be unique per branch. Both/neither of `command`/`executable` are still
|
|
555
|
+
* refused, by the strict objects themselves.
|
|
556
|
+
*/
|
|
557
|
+
export const backgroundExecInput = z.union([
|
|
558
|
+
shellStartInput,
|
|
559
|
+
directStartInput,
|
|
560
|
+
readInput.omit({ offset: true }).extend({
|
|
561
|
+
operation: z.literal("poll"),
|
|
562
|
+
// Omission reads the tail; a returned nextOffset resumes output explicitly.
|
|
563
|
+
offset: z.number().int().nonnegative().optional(),
|
|
564
|
+
}),
|
|
565
|
+
waitInput.extend({
|
|
566
|
+
operation: z.literal("wait"),
|
|
567
|
+
offset: z.number().int().nonnegative().optional(),
|
|
568
|
+
maxBytes: readInput.shape.maxBytes,
|
|
569
|
+
}),
|
|
570
|
+
idInput.extend({ operation: z.literal("kill") }),
|
|
571
|
+
z.strictObject({ operation: z.literal("list") }),
|
|
572
|
+
]);
|
|
573
|
+
const backgroundResult = readOutput.extend({
|
|
574
|
+
timedOut: z.boolean(),
|
|
575
|
+
signalled: z.boolean().optional(),
|
|
576
|
+
});
|
|
577
|
+
export const backgroundExecOutput = z.union([backgroundResult, listOutput]);
|
|
578
|
+
/** Reuse Session ownership and logs; a wait deadline never cancels the command. */
|
|
579
|
+
export async function callBackgroundExec(scope, request, resolveCwd, executionPath, signal, sandbox) {
|
|
580
|
+
signal.throwIfAborted();
|
|
581
|
+
if (request.operation === "list")
|
|
582
|
+
return { processes: [...scope.list()] };
|
|
583
|
+
let id;
|
|
584
|
+
let timedOut = false;
|
|
585
|
+
let signalled;
|
|
586
|
+
switch (request.operation) {
|
|
587
|
+
case "start": {
|
|
588
|
+
const cwd = await resolveCwd(request.cwd);
|
|
589
|
+
signal.throwIfAborted();
|
|
590
|
+
id = (await scope.start(toCommandSpec(request), cwd, executionPath, sandbox)).id;
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
case "wait":
|
|
594
|
+
id = request.id;
|
|
595
|
+
timedOut = (await scope.wait(id, request.timeoutMs, signal)).timedOut;
|
|
596
|
+
break;
|
|
597
|
+
case "kill":
|
|
598
|
+
id = request.id;
|
|
599
|
+
signalled = (await scope.kill(id)).signalled;
|
|
600
|
+
break;
|
|
601
|
+
case "poll":
|
|
602
|
+
id = request.id;
|
|
603
|
+
break;
|
|
604
|
+
}
|
|
605
|
+
const maxBytes = "maxBytes" in request ? request.maxBytes : defaultReadBytes;
|
|
606
|
+
const offset = "offset" in request ? request.offset : undefined;
|
|
607
|
+
const from = offset ?? Math.max(0, scope.status(id).logBytes - maxBytes);
|
|
608
|
+
const output = await scope.read(id, from, maxBytes);
|
|
609
|
+
const result = { ...output, timedOut };
|
|
610
|
+
if (signalled !== undefined)
|
|
611
|
+
result.signalled = signalled;
|
|
612
|
+
return backgroundResult.parse(result);
|
|
613
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The host's selected user home, not an Agent Home or an invocation Files jail.
|
|
3
|
+
* Pass these values through the existing home/path parameters. Capture once at
|
|
4
|
+
* the outer host/client boundary, before awaiting or constructing lazy owners;
|
|
5
|
+
* later opens, reloads and exec launches must not select ambient HOME again.
|
|
6
|
+
*
|
|
7
|
+
* PATH remains the existing per-client/per-Session choice. Neither this value
|
|
8
|
+
* nor an exec child inherits the rest of the host environment.
|
|
9
|
+
*/
|
|
10
|
+
export type PrivateLocalConfigRoot = Readonly<{
|
|
11
|
+
home: string;
|
|
12
|
+
credentialPath: string;
|
|
13
|
+
serviceDirectory: string;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* Select paths without reading credentials, creating directories or starting a
|
|
17
|
+
* service. Keep this capture independent of service/discovery owners, which
|
|
18
|
+
* consume its paths. Explicit path overrides remain the caller's responsibility;
|
|
19
|
+
* there is no settings registry, fallback chain or alternate storage format.
|
|
20
|
+
*/
|
|
21
|
+
export declare function capturePrivateLocalConfigRoot(home?: string): PrivateLocalConfigRoot;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
3
|
+
import { resolveDefaultCredentialPath } from "./private-file-credential-store.js";
|
|
4
|
+
/**
|
|
5
|
+
* Select paths without reading credentials, creating directories or starting a
|
|
6
|
+
* service. Keep this capture independent of service/discovery owners, which
|
|
7
|
+
* consume its paths. Explicit path overrides remain the caller's responsibility;
|
|
8
|
+
* there is no settings registry, fallback chain or alternate storage format.
|
|
9
|
+
*/
|
|
10
|
+
export function capturePrivateLocalConfigRoot(home = homedir()) {
|
|
11
|
+
if (!isAbsolute(home))
|
|
12
|
+
throw new Error("Local configuration home must be absolute");
|
|
13
|
+
const selected = resolve(home);
|
|
14
|
+
return Object.freeze({
|
|
15
|
+
home: selected,
|
|
16
|
+
credentialPath: resolveDefaultCredentialPath(selected),
|
|
17
|
+
serviceDirectory: join(selected, ".fungi", "service"),
|
|
18
|
+
});
|
|
19
|
+
}
|