@fungi.computer/truffle 0.0.0-stage → 0.1.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.
Files changed (116) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -2
  3. package/computerd-loopback.cjs +16 -0
  4. package/dist/auth-cli.d.ts +36 -0
  5. package/dist/auth-cli.js +351 -0
  6. package/dist/fungi-account-command.d.ts +18 -0
  7. package/dist/fungi-account-command.js +92 -0
  8. package/dist/index.d.ts +19 -0
  9. package/dist/index.js +17 -0
  10. package/dist/internal/private-workerd-sandbox-worker.mjs +27488 -0
  11. package/dist/internal/sandbox-metafile.json +6726 -0
  12. package/dist/linear-auth.d.ts +4 -0
  13. package/dist/linear-auth.js +72 -0
  14. package/dist/local-extensions.d.ts +97 -0
  15. package/dist/local-extensions.js +484 -0
  16. package/dist/local-login.d.ts +55 -0
  17. package/dist/local-login.js +167 -0
  18. package/dist/local-models.d.ts +14 -0
  19. package/dist/local-models.js +77 -0
  20. package/dist/local-skill-source.d.ts +6 -0
  21. package/dist/local-skill-source.js +357 -0
  22. package/dist/log-event.d.ts +2 -0
  23. package/dist/log-event.js +9 -0
  24. package/dist/private-bun-shiitake-host.d.ts +11 -0
  25. package/dist/private-bun-shiitake-host.js +66 -0
  26. package/dist/private-file-credential-store.d.ts +33 -0
  27. package/dist/private-file-credential-store.js +283 -0
  28. package/dist/private-files-patch.d.ts +3 -0
  29. package/dist/private-files-patch.js +86 -0
  30. package/dist/private-fungi-account.d.ts +235 -0
  31. package/dist/private-fungi-account.js +1149 -0
  32. package/dist/private-fungi-auth-error.d.ts +5 -0
  33. package/dist/private-fungi-auth-error.js +7 -0
  34. package/dist/private-fungi-mcp-connection.d.ts +22 -0
  35. package/dist/private-fungi-mcp-connection.js +143 -0
  36. package/dist/private-fungi-mcp-endpoint.d.ts +2 -0
  37. package/dist/private-fungi-mcp-endpoint.js +15 -0
  38. package/dist/private-fungi-mcp-selection.d.ts +14 -0
  39. package/dist/private-fungi-mcp-selection.js +36 -0
  40. package/dist/private-fungi-model-api.d.ts +6 -0
  41. package/dist/private-fungi-model-api.js +125 -0
  42. package/dist/private-fungi-model-catalog.d.ts +6 -0
  43. package/dist/private-fungi-model-catalog.js +78 -0
  44. package/dist/private-fungi-model-provider.d.ts +6 -0
  45. package/dist/private-fungi-model-provider.js +46 -0
  46. package/dist/private-fungi-pending-logouts.d.ts +34 -0
  47. package/dist/private-fungi-pending-logouts.js +136 -0
  48. package/dist/private-invocation-computer.d.ts +37 -0
  49. package/dist/private-invocation-computer.js +459 -0
  50. package/dist/private-invocation-files.d.ts +15 -0
  51. package/dist/private-invocation-files.js +449 -0
  52. package/dist/private-local-acp-stdio.d.ts +17 -0
  53. package/dist/private-local-acp-stdio.js +142 -0
  54. package/dist/private-local-auth-rpc.d.ts +69 -0
  55. package/dist/private-local-auth-rpc.js +273 -0
  56. package/dist/private-local-computer-attach.d.ts +49 -0
  57. package/dist/private-local-computer-attach.js +216 -0
  58. package/dist/private-local-computer-backend.d.ts +282 -0
  59. package/dist/private-local-computer-backend.js +402 -0
  60. package/dist/private-local-computer-processes.d.ts +199 -0
  61. package/dist/private-local-computer-processes.js +589 -0
  62. package/dist/private-local-default-model.d.ts +11 -0
  63. package/dist/private-local-default-model.js +45 -0
  64. package/dist/private-local-delegation.d.ts +29 -0
  65. package/dist/private-local-delegation.js +78 -0
  66. package/dist/private-local-effective-model.d.ts +19 -0
  67. package/dist/private-local-effective-model.js +14 -0
  68. package/dist/private-local-exec-launch.d.ts +41 -0
  69. package/dist/private-local-exec-launch.js +51 -0
  70. package/dist/private-local-exec-path.d.ts +6 -0
  71. package/dist/private-local-exec-path.js +40 -0
  72. package/dist/private-local-git-exclude.d.ts +2 -0
  73. package/dist/private-local-git-exclude.js +33 -0
  74. package/dist/private-local-http-dispatch.d.ts +5 -0
  75. package/dist/private-local-http-dispatch.js +14 -0
  76. package/dist/private-local-mail.d.ts +11 -0
  77. package/dist/private-local-mail.js +69 -0
  78. package/dist/private-local-model-settings.d.ts +5 -0
  79. package/dist/private-local-model-settings.js +31 -0
  80. package/dist/private-local-runtime-host.d.ts +141 -0
  81. package/dist/private-local-runtime-host.js +988 -0
  82. package/dist/private-local-service-contract.d.ts +90 -0
  83. package/dist/private-local-service-contract.js +1 -0
  84. package/dist/private-local-service-discovery.d.ts +101 -0
  85. package/dist/private-local-service-discovery.js +221 -0
  86. package/dist/private-local-service-ids.d.ts +3 -0
  87. package/dist/private-local-service-ids.js +8 -0
  88. package/dist/private-local-service-listener.d.ts +15 -0
  89. package/dist/private-local-service-listener.js +211 -0
  90. package/dist/private-local-service-owner.d.ts +1567 -0
  91. package/dist/private-local-service-owner.js +371 -0
  92. package/dist/private-local-service-process.d.ts +1 -0
  93. package/dist/private-local-service-process.js +6 -0
  94. package/dist/private-local-service.d.ts +17 -0
  95. package/dist/private-local-service.js +330 -0
  96. package/dist/private-local-session-discovery.d.ts +19 -0
  97. package/dist/private-local-session-discovery.js +157 -0
  98. package/dist/private-local-shiitake-host.d.ts +44 -0
  99. package/dist/private-local-shiitake-host.js +143 -0
  100. package/dist/private-local-subagents.d.ts +27 -0
  101. package/dist/private-local-subagents.js +77 -0
  102. package/dist/private-local-worker-server.d.ts +15 -0
  103. package/dist/private-local-worker-server.js +136 -0
  104. package/dist/private-sqlite-owner.d.ts +7 -0
  105. package/dist/private-sqlite-owner.js +103 -0
  106. package/dist/private-team-computers.d.ts +38 -0
  107. package/dist/private-team-computers.js +101 -0
  108. package/dist/private-workerd-sandbox.d.ts +3 -0
  109. package/dist/private-workerd-sandbox.js +347 -0
  110. package/dist/team-apps.d.ts +24 -0
  111. package/dist/team-apps.js +123 -0
  112. package/dist/tracker-git-credential.d.ts +15 -0
  113. package/dist/tracker-git-credential.js +87 -0
  114. package/native/bin/build.json +4037 -0
  115. package/native/bin/fungi-apply-patch +0 -0
  116. package/package.json +148 -3
@@ -0,0 +1,78 @@
1
+ import { deterministicReplayKey } from "@fungi.computer/cairn";
2
+ import { createFleetDelegation, delegationProvenance, delegationReplyMeta, delegationSendMeta, } from "@fungi.computer/shiitake/fleet";
3
+ /** How long one Local delegation grant stays valid while it is admitted. */
4
+ const grantLifetimeMs = 60_000;
5
+ /**
6
+ * One Local Agent's side of delegation, over the same fleet contract as the
7
+ * Hub: a `followUp` send freezes BirdDog mail in this Agent's store, and an
8
+ * arriving reply is admitted only after `fleet.authenticateReply` checks it
9
+ * against that frozen mail. The replying child authors the reply line.
10
+ */
11
+ export function createPrivateLocalDelegation(input) {
12
+ const fleet = createFleetDelegation({
13
+ storage: input.storage,
14
+ deliver: input.routing.deliver,
15
+ readSettlement: input.routing.readSettlement,
16
+ open: (agentId) => input.directory.open(agentId, new AbortController().signal),
17
+ });
18
+ async function members(signal) {
19
+ return (await input.directory.list(signal)).map(({ agentId, name }) => ({
20
+ agentId,
21
+ name,
22
+ }));
23
+ }
24
+ /** Delegate follow-up work; its reply returns to the invoking Session. */
25
+ async function send(task, sourceSessionId, signal, invocation) {
26
+ if (invocation === undefined)
27
+ throw new Error("Delegation requires a tool invocation");
28
+ const agents = await members(signal);
29
+ if (!agents.some(({ agentId }) => agentId === task.agentId))
30
+ throw new Error("Agent is outside this Local project");
31
+ const meta = delegationSendMeta({ id: input.agentId, name: input.agentId }, invocation.meta);
32
+ const provenance = await delegationProvenance({ agentId: input.agentId, sessionId: sourceSessionId }, invocation.toolCallCode);
33
+ const requestId = await deterministicReplayKey("local-agent-delegation", [
34
+ input.agentId,
35
+ sourceSessionId,
36
+ invocation.commandId ?? "",
37
+ invocation.toolCallId,
38
+ String(invocation.sendIndex),
39
+ ]);
40
+ signal.throwIfAborted();
41
+ return fleet.start({
42
+ agents: agents.map(({ agentId }) => agentId),
43
+ actions: ["delegate"],
44
+ expiresAt: Date.now() + grantLifetimeMs,
45
+ provenance,
46
+ meta,
47
+ }, { ...task, requestId });
48
+ }
49
+ /**
50
+ * Admit one mail addressed to this Agent. New work must name its sender as
51
+ * its only reply destination; a reply-less mail is admitted only as an
52
+ * authenticated reply. Everything else is refused.
53
+ */
54
+ async function receive(mail) {
55
+ if (mail.to.agentId !== input.agentId)
56
+ throw new Error("Local addressed mail recipient is not authorized");
57
+ const agents = await members(new AbortController().signal);
58
+ if (!agents.some(({ agentId }) => agentId === mail.from.agentId))
59
+ throw new Error("Local mail sender is outside this project");
60
+ if (mail.replyTo.length > 0) {
61
+ const [destination] = mail.replyTo;
62
+ if (mail.replyTo.length !== 1 ||
63
+ destination?.agentId !== mail.from.agentId ||
64
+ destination.sessionId !== mail.from.sessionId)
65
+ throw new Error("Local delegated work must reply to its sender");
66
+ return input.accept(mail, mail.meta);
67
+ }
68
+ const delegation = fleet.authenticateReply(mail);
69
+ if (delegation === undefined)
70
+ throw new Error("Local reply does not match work this Agent sent");
71
+ const replier = agents.find(({ agentId }) => agentId === delegation.reply.agentId);
72
+ if (replier === undefined)
73
+ throw new Error("Local reply sender is outside this project");
74
+ const meta = delegationReplyMeta(delegation, replier.name);
75
+ return input.accept({ ...mail, meta }, meta, delegation.reply);
76
+ }
77
+ return { send, receive };
78
+ }
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ /** Secret-free model status published by the Local owner for one Session. */
3
+ export declare const privateLocalEffectiveModelSchema: z.ZodObject<{
4
+ agentName: z.ZodLiteral<"Shiitake">;
5
+ providerId: z.ZodString;
6
+ providerName: z.ZodString;
7
+ modelId: z.ZodString;
8
+ modelName: z.ZodString;
9
+ credential: z.ZodObject<{
10
+ kind: z.ZodEnum<{
11
+ api_key: "api_key";
12
+ oauth: "oauth";
13
+ missing: "missing";
14
+ }>;
15
+ source: z.ZodString;
16
+ }, z.core.$strict>;
17
+ label: z.ZodString;
18
+ }, z.core.$strict>;
19
+ export type PrivateLocalEffectiveModel = z.output<typeof privateLocalEffectiveModelSchema>;
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ /** Secret-free model status published by the Local owner for one Session. */
3
+ export const privateLocalEffectiveModelSchema = z.strictObject({
4
+ agentName: z.literal("Shiitake"),
5
+ providerId: z.string().min(1).max(160),
6
+ providerName: z.string().min(1).max(256),
7
+ modelId: z.string().min(1).max(160),
8
+ modelName: z.string().min(1).max(256),
9
+ credential: z.strictObject({
10
+ kind: z.enum(["oauth", "api_key", "missing"]),
11
+ source: z.string().min(1).max(160),
12
+ }),
13
+ label: z.string().min(1).max(640),
14
+ });
@@ -0,0 +1,41 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * One exec launch's own text ceiling: 16384, shared by the shell form's
4
+ * `command` and the direct form's combined `executable`+`args`, so neither
5
+ * gets a materially larger budget. This is exec's own bound, not borrowed
6
+ * from any other owner's limit.
7
+ *
8
+ * The two forms count against it in different units, which the name
9
+ * deliberately leaves open rather than mis-claiming one: `command` is
10
+ * bounded by `z.string().max()`, zod's native UTF-16 code-unit count, the
11
+ * same as the field already was before the direct form existed. The direct
12
+ * form's `combinedArgvBytes` below counts real UTF-8 bytes, because argv is
13
+ * a byte protocol with no code-unit concept. For multi-byte text the two
14
+ * measures diverge; both stay well inside this ceiling for any launch a
15
+ * model would actually send.
16
+ */
17
+ export declare const maximumLaunchTextLength: number;
18
+ /**
19
+ * One exec launch's own stdin bound. A launch's stdin is a finite payload
20
+ * held fully in memory and written to the child once, then closed; it is
21
+ * never streamed, paginated or interactive. 128 KiB keeps that one-shot
22
+ * payload bounded while still fitting an ordinary small script or data blob.
23
+ * This is exec's own bound, not borrowed from any other owner's limit (in
24
+ * particular, not Files' write bound, which governs a different resource:
25
+ * durable file content, read and written in pages).
26
+ */
27
+ export declare const maximumLaunchStdinBytes: number;
28
+ /**
29
+ * Finite UTF-8 text delivered to a launched child once, then closed; never
30
+ * an interactive protocol. A lone surrogate is refused rather than silently
31
+ * becoming U+FFFD, so byte-identity is a real guarantee, not a best effort.
32
+ */
33
+ export declare const launchStdinSchema: z.ZodString;
34
+ /** Combined UTF-8 bytes of a direct form's executable and its args. */
35
+ export declare function combinedArgvBytes(executable: string, args: readonly string[]): number;
36
+ /**
37
+ * One launch's requested `cwd`: absolute, bounded, no NUL. The caller still
38
+ * resolves and contains it under the invocation root; this schema only
39
+ * shapes the raw string.
40
+ */
41
+ export declare const launchCwdSchema: z.ZodString;
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * One exec launch's own text ceiling: 16384, shared by the shell form's
4
+ * `command` and the direct form's combined `executable`+`args`, so neither
5
+ * gets a materially larger budget. This is exec's own bound, not borrowed
6
+ * from any other owner's limit.
7
+ *
8
+ * The two forms count against it in different units, which the name
9
+ * deliberately leaves open rather than mis-claiming one: `command` is
10
+ * bounded by `z.string().max()`, zod's native UTF-16 code-unit count, the
11
+ * same as the field already was before the direct form existed. The direct
12
+ * form's `combinedArgvBytes` below counts real UTF-8 bytes, because argv is
13
+ * a byte protocol with no code-unit concept. For multi-byte text the two
14
+ * measures diverge; both stay well inside this ceiling for any launch a
15
+ * model would actually send.
16
+ */
17
+ export const maximumLaunchTextLength = 16 * 1_024;
18
+ /**
19
+ * One exec launch's own stdin bound. A launch's stdin is a finite payload
20
+ * held fully in memory and written to the child once, then closed; it is
21
+ * never streamed, paginated or interactive. 128 KiB keeps that one-shot
22
+ * payload bounded while still fitting an ordinary small script or data blob.
23
+ * This is exec's own bound, not borrowed from any other owner's limit (in
24
+ * particular, not Files' write bound, which governs a different resource:
25
+ * durable file content, read and written in pages).
26
+ */
27
+ export const maximumLaunchStdinBytes = 128 * 1_024;
28
+ /**
29
+ * Finite UTF-8 text delivered to a launched child once, then closed; never
30
+ * an interactive protocol. A lone surrogate is refused rather than silently
31
+ * becoming U+FFFD, so byte-identity is a real guarantee, not a best effort.
32
+ */
33
+ export const launchStdinSchema = z
34
+ .string()
35
+ .refine((value) => value.isWellFormed(), "Computer exec stdin must be well-formed UTF-16 text (no lone surrogates)")
36
+ .refine((value) => Buffer.byteLength(value, "utf8") <= maximumLaunchStdinBytes, `Computer exec stdin exceeds the ${String(maximumLaunchStdinBytes)}-byte bound`);
37
+ /** Combined UTF-8 bytes of a direct form's executable and its args. */
38
+ export function combinedArgvBytes(executable, args) {
39
+ return [executable, ...args].reduce((total, value) => total + Buffer.byteLength(value, "utf8"), 0);
40
+ }
41
+ /**
42
+ * One launch's requested `cwd`: absolute, bounded, no NUL. The caller still
43
+ * resolves and contains it under the invocation root; this schema only
44
+ * shapes the raw string.
45
+ */
46
+ export const launchCwdSchema = z
47
+ .string()
48
+ .min(1)
49
+ .max(4_096)
50
+ .startsWith("/")
51
+ .refine((path) => !path.includes("\0"), "Path cannot contain null bytes");
@@ -0,0 +1,6 @@
1
+ import { z } from "zod";
2
+ /** Only the invoking client's bounded PATH crosses the local service boundary. */
3
+ export declare const privateLocalExecPathSchema: z.ZodString;
4
+ export declare function parsePrivateLocalExecPath(value: string): string;
5
+ /** Resolve shell-relative client entries before the absolute-only RPC boundary. */
6
+ export declare function capturePrivateLocalExecPath(value: string, clientDirectory: string): string;
@@ -0,0 +1,40 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+ import { z } from "zod";
3
+ const maximumPathCharacters = 8_192;
4
+ const maximumPathDirectories = 64;
5
+ /** Only the invoking client's bounded PATH crosses the local service boundary. */
6
+ export const privateLocalExecPathSchema = z
7
+ .string()
8
+ .min(1)
9
+ .max(maximumPathCharacters)
10
+ .superRefine((path, context) => {
11
+ const directories = path.split(":");
12
+ if (directories.length > maximumPathDirectories)
13
+ context.addIssue({
14
+ code: "custom",
15
+ message: "PATH has too many directories",
16
+ });
17
+ for (const directory of directories) {
18
+ if (directory === "" ||
19
+ !isAbsolute(directory) ||
20
+ /[\p{Cc}\p{Cf}]/u.test(directory)) {
21
+ context.addIssue({
22
+ code: "custom",
23
+ message: "PATH must contain only nonempty absolute directories",
24
+ });
25
+ break;
26
+ }
27
+ }
28
+ });
29
+ export function parsePrivateLocalExecPath(value) {
30
+ return privateLocalExecPathSchema.parse(value);
31
+ }
32
+ /** Resolve shell-relative client entries before the absolute-only RPC boundary. */
33
+ export function capturePrivateLocalExecPath(value, clientDirectory) {
34
+ const absolute = value
35
+ .split(":")
36
+ .map((directory) => directory === "" || isAbsolute(directory)
37
+ ? directory
38
+ : resolve(clientDirectory, directory));
39
+ return parsePrivateLocalExecPath(absolute.join(":"));
40
+ }
@@ -0,0 +1,2 @@
1
+ /** Keep generated project state out of Git without changing tracked ignore rules. */
2
+ export declare function ensurePrivateLocalGitExclude(projectRoot: string): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import { execFile as execFileCallback } from "node:child_process";
2
+ import { appendFile, mkdir, readFile } from "node:fs/promises";
3
+ import { resolve, join } from "node:path";
4
+ import { promisify } from "node:util";
5
+ const execFile = promisify(execFileCallback);
6
+ const excludeEntry = ".fungi/";
7
+ /** Keep generated project state out of Git without changing tracked ignore rules. */
8
+ export async function ensurePrivateLocalGitExclude(projectRoot) {
9
+ let common;
10
+ try {
11
+ const result = await execFile("git", ["rev-parse", "--git-common-dir"], {
12
+ cwd: projectRoot,
13
+ });
14
+ common = resolve(projectRoot, result.stdout.trim());
15
+ }
16
+ catch (cause) {
17
+ // A non-Git project still keeps its local Fungi store.
18
+ if (cause instanceof Error && "code" in cause && cause.code === 128)
19
+ return;
20
+ throw cause;
21
+ }
22
+ const info = join(common, "info");
23
+ await mkdir(info, { recursive: true });
24
+ const exclude = join(info, "exclude");
25
+ const current = await readFile(exclude, "utf8").catch((cause) => {
26
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
27
+ return "";
28
+ throw cause;
29
+ });
30
+ if (current.split(/\r?\n/u).some((line) => line.trim() === excludeEntry))
31
+ return;
32
+ await appendFile(exclude, `${current !== "" && !current.endsWith("\n") ? "\n" : ""}${excludeEntry}\n`);
33
+ }
@@ -0,0 +1,5 @@
1
+ export type RequestIdleClock = {
2
+ timeout: (request: Request, seconds: number) => void;
3
+ };
4
+ /** Bun's socket clock must stay off while a command or quiet SSE is pending. */
5
+ export declare function dispatchPrivateLocalRequest(request: Request, clock: RequestIdleClock, fetch: (request: Request) => Response | Promise<Response>): Promise<Response>;
@@ -0,0 +1,14 @@
1
+ /** Bun's socket clock must stay off while a command or quiet SSE is pending. */
2
+ export async function dispatchPrivateLocalRequest(request, clock, fetch) {
3
+ clock.timeout(request, 0);
4
+ try {
5
+ const response = await fetch(request);
6
+ if (!response.headers.get("content-type")?.startsWith("text/event-stream"))
7
+ clock.timeout(request, 10);
8
+ return response;
9
+ }
10
+ catch (cause) {
11
+ clock.timeout(request, 10);
12
+ throw cause;
13
+ }
14
+ }
@@ -0,0 +1,11 @@
1
+ import type { BirdDogMail } from "@fungi.computer/bird-dog";
2
+ import { type BirdDogEffect } from "@fungi.computer/bird-dog/effect";
3
+ import type { DatabaseSync } from "node:sqlite";
4
+ import type { Accepted } from "@fungi.computer/shiitake";
5
+ export type PrivateLocalMailOptions = Readonly<{
6
+ /** Admit one reply at its addressed Agent; a rejection retries later. */
7
+ deliver: (mail: BirdDogMail) => Promise<Accepted>;
8
+ scheduleWake?: (at: number) => void;
9
+ now?: () => number;
10
+ }>;
11
+ export declare function createPrivateLocalMailRoom(database: DatabaseSync, options: PrivateLocalMailOptions): BirdDogEffect;
@@ -0,0 +1,69 @@
1
+ import { BirdDogError, make as makeBirdDog, } from "@fungi.computer/bird-dog/effect";
2
+ import * as Effect from "effect/Effect";
3
+ import { messageAuthor } from "@fungi.computer/agar/agents";
4
+ import { z } from "zod";
5
+ const outboxValue = z.object({ value: z.string() });
6
+ const outboxRows = z.array(z.object({ key: z.string(), value: z.string() }).readonly());
7
+ function storeFailure(cause) {
8
+ return new BirdDogError("local mail store is unavailable", { cause });
9
+ }
10
+ export function createPrivateLocalMailRoom(database, options) {
11
+ database.exec("CREATE TABLE IF NOT EXISTS local_birddog_outbox (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL)");
12
+ const room = Effect.runSync(makeBirdDog({
13
+ outbox: {
14
+ read: (key) => Effect.try({
15
+ try: () => {
16
+ const statement = database.prepare("SELECT value FROM local_birddog_outbox WHERE key = ?");
17
+ const row = outboxValue.safeParse(statement.get(key));
18
+ return row.success ? row.data.value : undefined;
19
+ },
20
+ catch: storeFailure,
21
+ }),
22
+ put: (key, value) => Effect.try({
23
+ try: () => {
24
+ database
25
+ .prepare("INSERT INTO local_birddog_outbox (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value")
26
+ .run(key, value);
27
+ },
28
+ catch: storeFailure,
29
+ }),
30
+ list: (limit, afterKey) => Effect.try({
31
+ try: () => outboxRows.parse(database
32
+ .prepare("SELECT key, value FROM local_birddog_outbox WHERE (? IS NULL OR key > ?) ORDER BY key LIMIT ?")
33
+ .all(afterKey ?? null, afterKey ?? null, limit)),
34
+ catch: storeFailure,
35
+ }),
36
+ delete: (key) => Effect.try({
37
+ try: () => {
38
+ database
39
+ .prepare("DELETE FROM local_birddog_outbox WHERE key = ?")
40
+ .run(key);
41
+ },
42
+ catch: storeFailure,
43
+ }),
44
+ },
45
+ post: (mail) => Effect.tryPromise({
46
+ try: async () => {
47
+ await options.deliver(mail);
48
+ return true;
49
+ },
50
+ catch: (cause) => new BirdDogError("local recipient rejected mail", { cause }),
51
+ }).pipe(Effect.catch((cause) => Effect.logError("Local mail recipient rejected delivery", cause).pipe(Effect.as(false)))),
52
+ scheduleWake: (at) => Effect.sync(() => options.scheduleWake?.(at)),
53
+ now: options.now ?? (() => Date.now()),
54
+ }));
55
+ return {
56
+ ...room,
57
+ fanOut: (settlement) => {
58
+ if (settlement.job.outcome !== "cancelled")
59
+ return room.fanOut(settlement);
60
+ const author = messageAuthor(settlement.meta);
61
+ return room.fanOut({
62
+ ...settlement,
63
+ content: author === undefined
64
+ ? "Work cancelled."
65
+ : `Work cancelled by ${author.name}.`,
66
+ });
67
+ },
68
+ };
69
+ }
@@ -0,0 +1,5 @@
1
+ import { type ModelPreference } from "@fungi.computer/shiitake/model-preference";
2
+ /** Reads Local's per-person remembered model; a Session switch never writes it. */
3
+ export declare function createLocalModelSettings(path?: string): {
4
+ read: () => Promise<ModelPreference | null>;
5
+ };
@@ -0,0 +1,31 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import * as Schema from "effect/Schema";
5
+ import { ModelPreferenceSchema, } from "@fungi.computer/shiitake/model-preference";
6
+ const settingsSchema = Schema.Struct({
7
+ lastManualModel: ModelPreferenceSchema,
8
+ });
9
+ const decodeSettings = Schema.decodeUnknownSync(Schema.fromJsonString(settingsSchema), { onExcessProperty: "error" });
10
+ /** Reads Local's per-person remembered model; a Session switch never writes it. */
11
+ export function createLocalModelSettings(path = join(homedir(), ".config", "fungi", "shiitake.json")) {
12
+ return {
13
+ read: async () => {
14
+ try {
15
+ const text = await readFile(path, "utf8");
16
+ if (text.length > 4096)
17
+ throw new Error("Local model settings exceed their bound");
18
+ return decodeSettings(text).lastManualModel;
19
+ }
20
+ catch (cause) {
21
+ if (cause instanceof Error &&
22
+ "code" in cause &&
23
+ cause.code === "ENOENT")
24
+ return null;
25
+ throw new Error("Local model settings are invalid or unreadable", {
26
+ cause,
27
+ });
28
+ }
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,141 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { type Models, type ThinkingLevel } from "@earendil-works/pi-ai";
3
+ import type { CapabilitySource } from "@fungi.computer/mycelium";
4
+ import type { BirdDogMail } from "@fungi.computer/bird-dog";
5
+ import type { Accepted, CommandSettlement } from "@fungi.computer/shiitake";
6
+ import { type ModelPreference } from "@fungi.computer/shiitake/model-preference";
7
+ import { type AgentControlDirectory } from "@fungi.computer/shiitake/agent-control";
8
+ import type { ExtensionRosterV0 } from "@fungi.computer/shiitake/extensions";
9
+ import { type InstructionSource, type ModelSource, type RuntimeHost, type ShiitakeAgent, type ShiitakeSession, type ShiitakeOptions } from "@fungi.computer/shiitake";
10
+ import { type PrivateLocalEffectiveModel } from "./private-local-effective-model.js";
11
+ import { type PrivateLocalComputerProcesses } from "./private-local-computer-processes.js";
12
+ import { type NodeRuntimeHost, type NodeStoreLease } from "@fungi.computer/shiitake/node";
13
+ import { createPrivateInvocationFilesPort } from "./private-invocation-files.js";
14
+ import { type PrivateFungiMcpConnectionOptions } from "./private-fungi-mcp-connection.js";
15
+ import { type PrivateLocalMailRouting } from "./private-local-delegation.js";
16
+ import { type SubagentSource } from "@fungi.computer/shiitake-extensions/subagents/host";
17
+ declare const localHostIds: Readonly<{
18
+ readonly store: import("@fungi.computer/cairn").IdCodec<"local-host", "store", 1>;
19
+ readonly "session-create-request": import("@fungi.computer/cairn").IdCodec<"local-host", "session-create-request", 1>;
20
+ }>;
21
+ export type LocalStoreId = ReturnType<typeof localHostIds.store.generate>;
22
+ export type LocalResources = Readonly<{
23
+ agent?: Pick<ShiitakeAgent, "close"> | undefined;
24
+ processes?: Pick<PrivateLocalComputerProcesses, "close"> | undefined;
25
+ runtime: Omit<NodeRuntimeHost, "scheduleWake">;
26
+ database?: Pick<DatabaseSync, "close"> | undefined;
27
+ lease?: NodeStoreLease | undefined;
28
+ providerCacheKey?: string | undefined;
29
+ }>;
30
+ export type PrivateLocalTools = Readonly<{
31
+ port: Awaited<ReturnType<typeof createPrivateInvocationFilesPort>>;
32
+ tools: NonNullable<ShiitakeOptions["tools"]>;
33
+ extensions: ExtensionRosterV0;
34
+ /** Background Computer processes, scoped per Session; the host closes them. */
35
+ processes: PrivateLocalComputerProcesses;
36
+ }>;
37
+ /** Inputs for the one private local Agent opener. */
38
+ export type PrivateLocalRuntimeHostOptions = Readonly<{
39
+ /** Invocation root captured before selecting a fresh local database. */
40
+ invocationRoot: string;
41
+ /** Existing Agent database to reopen instead of creating a fresh local store. */
42
+ databasePath?: string | undefined;
43
+ /** Stable Local store identity for replaying one Agent create request. */
44
+ createRequestId?: string | undefined;
45
+ /** Explicit Agent model; otherwise inherit manual choice and available login. */
46
+ providerId?: string | undefined;
47
+ modelId?: string | undefined;
48
+ reasoning?: ThinkingLevel | undefined;
49
+ /** Local settings location; credentials remain in their separate owner. */
50
+ modelSettingsPath?: string | undefined;
51
+ /**
52
+ * Service-owned project mail routing. With `agentControl`, it gives this
53
+ * Agent follow-up delegation with authenticated replies and settles its own
54
+ * addressed work back to the sender.
55
+ */
56
+ mail?: PrivateLocalMailRouting | undefined;
57
+ /** Service-owned directory scoped to the captured project. */
58
+ agentControl?: AgentControlDirectory | undefined;
59
+ /** Explicit hosted capability connection; local tools and the Agent store stay local. */
60
+ fungiMcp?: PrivateFungiMcpConnectionOptions | undefined;
61
+ }>;
62
+ /** Typed Session and awaited resource owner supplied directly to the local TUI. */
63
+ export type PrivateLocalRuntimeHost = Readonly<{
64
+ /** Stable Local directory identity of this authorized Agent. */
65
+ agentId: string;
66
+ /** Canonical file path to pass explicitly to a later --db launch. */
67
+ databasePath: string;
68
+ session: ShiitakeSession;
69
+ /** Address a Session through this same Agent and resource owner. */
70
+ getSession: (sessionId: string) => ShiitakeSession;
71
+ /** A selected Session keeps one client's command PATH while this host is live. */
72
+ bindSessionPath: (sessionId: string, path: string) => boolean;
73
+ /** Resolve this Session's effective model and nonsecret credential source. */
74
+ effectiveModel: (sessionId: string, signal: AbortSignal) => Promise<PrivateLocalEffectiveModel>;
75
+ /** Set the persistent Agent default without changing Session overrides. */
76
+ setModel: (model: ModelPreference, signal: AbortSignal) => Promise<void>;
77
+ /** Refuse unrunnable creation or send before claiming acceptance. */
78
+ assertModelReady: (sessionId: string, signal: AbortSignal) => Promise<void>;
79
+ listSessions: ShiitakeAgent["listSessions"];
80
+ createSession: ShiitakeAgent["createSession"];
81
+ deleteSession: ShiitakeAgent["deleteSession"];
82
+ /** List, resume and call this Agent's rostered extensions. */
83
+ extensions: ShiitakeAgent["extensions"];
84
+ /**
85
+ * Admit mail addressed to this Agent: new work that replies to its sender,
86
+ * or a reply authenticated against work this Agent sent. Anything else is
87
+ * refused.
88
+ */
89
+ acceptMail: (mail: BirdDogMail) => Promise<Accepted>;
90
+ /** Exact settled command facts for a delegating sender. */
91
+ readSettlement: (sessionId: string, commandId: string) => Promise<CommandSettlement | undefined>;
92
+ close: () => Promise<void>;
93
+ }>;
94
+ /** Local-owned id for a new Session creation request. */
95
+ export declare function createPrivateLocalSessionRequestId(): import("@fungi.computer/cairn").CairnId<"local-host", "session-create-request", 1>;
96
+ export type PrivateLocalCreateStore = Readonly<{
97
+ agentId: LocalStoreId;
98
+ databasePath: string;
99
+ }>;
100
+ /** Resolve an Agent create request through its durable Local receipt. */
101
+ export declare function resolvePrivateLocalCreateStore(root: string, requestId: string): Promise<PrivateLocalCreateStore>;
102
+ /** Build the fixed source read by Shiitake once for each local run. */
103
+ export declare function createPrivateLocalInstructionSource(port: Awaited<ReturnType<typeof createPrivateInvocationFilesPort>>): InstructionSource;
104
+ export declare function createPrivateLocalModelSource(models: Models, providerId: string | undefined, modelId: string | undefined, providerCacheKey: string, selectedOwnerReasoning?: ThinkingLevel, defaults?: Readonly<{
105
+ agent?: () => ModelPreference | null;
106
+ manual: () => Promise<ModelPreference | null>;
107
+ }>): ModelSource & Readonly<{
108
+ /** The secret-free effective selection the footer and /model show. */
109
+ effective: (preference: ModelPreference | null, signal: AbortSignal) => Promise<PrivateLocalEffectiveModel>;
110
+ assertReady: (preference: ModelPreference | null, signal: AbortSignal) => Promise<void>;
111
+ }>;
112
+ /** Assemble the existing Files, Computer, and workerd owners once per host. */
113
+ export declare function createPrivateLocalTools(invocationRoot: string, fungiMcp?: PrivateFungiMcpConnectionOptions, agentControl?: AgentControlDirectory, sessionPath?: (sessionId: string) => string | undefined,
114
+ /** This Agent's store; its owner-enabled installed extensions join the roster. */
115
+ storeDirectory?: string,
116
+ /**
117
+ * This Agent's sub-agent record: its Session-bound Agent-control tools and
118
+ * the view's facts. Present when it can direct other Agents.
119
+ */
120
+ subagents?: Readonly<{
121
+ control: CapabilitySource;
122
+ source: SubagentSource;
123
+ }>): Promise<PrivateLocalTools>;
124
+ /** Event-loop adaptation for the one local Shiitake owner; it has no queue policy. */
125
+ export declare function createPrivateLocalRuntimeHost(): RuntimeHost;
126
+ /**
127
+ * Close the fixed local owner in dependency order without surrendering its
128
+ * sidecar before runtime drain and main-database close have succeeded.
129
+ *
130
+ * This source-local seam is intentionally not re-exported by the package; the
131
+ * focused tests use it to falsify injected failure ordering.
132
+ */
133
+ export declare function closePrivateLocalResources(resources: LocalResources, initiatingFailure?: Error): Promise<void>;
134
+ /**
135
+ * Open one private file-backed local Shiitake host and its fixed Session.
136
+ *
137
+ * The returned close owns agent, main database, and sidecar release in that
138
+ * order. It is the only lifecycle the current terminal caller needs to learn.
139
+ */
140
+ export declare function openPrivateLocalRuntimeHost(input: PrivateLocalRuntimeHostOptions, models?: Models): Promise<PrivateLocalRuntimeHost>;
141
+ export {};