@fungi.computer/truffle 0.1.1 → 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.
Files changed (174) hide show
  1. package/README.md +282 -34
  2. package/dist/auth-cli.d.ts +10 -0
  3. package/dist/auth-cli.js +36 -14
  4. package/dist/fungi-account-command.d.ts +1 -1
  5. package/dist/fungi-account-command.js +6 -3
  6. package/dist/index.d.ts +10 -2
  7. package/dist/index.js +8 -2
  8. package/dist/internal/image-fit-metafile.json +106 -0
  9. package/dist/internal/local-image-fit-worker.mjs +960 -0
  10. package/dist/internal/private-workerd-sandbox-worker.mjs +11 -12
  11. package/dist/internal/sandbox-metafile.json +7 -7
  12. package/dist/local-archive-handoff.d.ts +12 -0
  13. package/dist/local-archive-handoff.js +21 -0
  14. package/dist/local-extensions.d.ts +58 -12
  15. package/dist/local-extensions.js +195 -55
  16. package/dist/local-image-fit.d.ts +30 -0
  17. package/dist/local-image-fit.js +105 -0
  18. package/dist/local-models.d.ts +4 -2
  19. package/dist/local-models.js +11 -6
  20. package/dist/local-session-archives.d.ts +8 -0
  21. package/dist/local-session-archives.js +152 -0
  22. package/dist/local-skill-source.d.ts +18 -3
  23. package/dist/local-skill-source.js +216 -140
  24. package/dist/log-event.d.ts +7 -0
  25. package/dist/log-event.js +8 -4
  26. package/dist/private-bounded-response.d.ts +2 -0
  27. package/dist/private-bounded-response.js +41 -0
  28. package/dist/private-file-credential-store.d.ts +28 -3
  29. package/dist/private-file-credential-store.js +57 -9
  30. package/dist/private-files-patch.d.ts +2 -1
  31. package/dist/private-files-patch.js +21 -3
  32. package/dist/private-fungi-account.d.ts +40 -23
  33. package/dist/private-fungi-account.js +201 -21
  34. package/dist/private-fungi-mcp-connection.d.ts +4 -0
  35. package/dist/private-fungi-mcp-connection.js +25 -6
  36. package/dist/private-fungi-mcp-selection-schema.d.ts +7 -0
  37. package/dist/private-fungi-mcp-selection-schema.js +15 -0
  38. package/dist/private-fungi-mcp-selection.d.ts +6 -6
  39. package/dist/private-fungi-mcp-selection.js +29 -16
  40. package/dist/private-fungi-model-api.js +4 -4
  41. package/dist/private-fungi-model-catalog.d.ts +10 -0
  42. package/dist/private-fungi-model-catalog.js +10 -2
  43. package/dist/private-fungi-model-provider.d.ts +1 -1
  44. package/dist/private-fungi-model-provider.js +6 -1
  45. package/dist/private-fungi-pending-logouts.d.ts +1 -1
  46. package/dist/private-fungi-pending-logouts.js +1 -2
  47. package/dist/private-fungi-service-logout.d.ts +44 -0
  48. package/dist/private-fungi-service-logout.js +59 -0
  49. package/dist/private-invocation-computer.d.ts +24 -3
  50. package/dist/private-invocation-computer.js +101 -62
  51. package/dist/private-invocation-files.d.ts +16 -1
  52. package/dist/private-invocation-files.js +154 -83
  53. package/dist/private-local-acp-stdio.js +3 -0
  54. package/dist/private-local-admission-gate.d.ts +14 -0
  55. package/dist/private-local-admission-gate.js +52 -0
  56. package/dist/private-local-agent-control.d.ts +101 -0
  57. package/dist/private-local-agent-control.js +30 -0
  58. package/dist/private-local-agent-default-model.d.ts +12 -0
  59. package/dist/private-local-agent-default-model.js +31 -0
  60. package/dist/private-local-auth-rpc.d.ts +67 -7
  61. package/dist/private-local-auth-rpc.js +140 -17
  62. package/dist/private-local-bwrap.d.ts +13 -0
  63. package/dist/private-local-bwrap.js +70 -0
  64. package/dist/private-local-client-version.d.ts +5 -0
  65. package/dist/private-local-client-version.js +43 -0
  66. package/dist/private-local-computer-attach.d.ts +3 -3
  67. package/dist/private-local-computer-attach.js +17 -3
  68. package/dist/private-local-computer-processes.d.ts +6 -3
  69. package/dist/private-local-computer-processes.js +55 -31
  70. package/dist/private-local-config-root.d.ts +21 -0
  71. package/dist/private-local-config-root.js +19 -0
  72. package/dist/private-local-containment.d.ts +51 -0
  73. package/dist/private-local-containment.js +161 -0
  74. package/dist/private-local-delegation.d.ts +10 -3
  75. package/dist/private-local-delegation.js +70 -15
  76. package/dist/private-local-effective-model.d.ts +9 -1
  77. package/dist/private-local-effective-model.js +3 -0
  78. package/dist/private-local-exec-environment.d.ts +16 -0
  79. package/dist/private-local-exec-environment.js +11 -0
  80. package/dist/private-local-linux-sandbox.d.ts +8 -0
  81. package/dist/private-local-linux-sandbox.js +67 -0
  82. package/dist/private-local-mail.d.ts +4 -2
  83. package/dist/private-local-mail.js +41 -33
  84. package/dist/private-local-mesh-enrollment.d.ts +34 -0
  85. package/dist/private-local-mesh-enrollment.js +188 -0
  86. package/dist/private-local-mesh-links.d.ts +21 -0
  87. package/dist/private-local-mesh-links.js +229 -0
  88. package/dist/private-local-mesh-record.d.ts +120 -0
  89. package/dist/private-local-mesh-record.js +55 -0
  90. package/dist/private-local-process-identity.d.ts +20 -0
  91. package/dist/private-local-process-identity.js +40 -0
  92. package/dist/private-local-runtime-host.d.ts +64 -13
  93. package/dist/private-local-runtime-host.js +361 -172
  94. package/dist/private-local-sandbox-policy.d.ts +21 -0
  95. package/dist/private-local-sandbox-policy.js +121 -0
  96. package/dist/private-local-sandbox.d.ts +37 -0
  97. package/dist/private-local-sandbox.js +114 -0
  98. package/dist/private-local-seatbelt.d.ts +24 -0
  99. package/dist/private-local-seatbelt.js +68 -0
  100. package/dist/private-local-service-contract.d.ts +91 -11
  101. package/dist/private-local-service-discovery.d.ts +129 -13
  102. package/dist/private-local-service-discovery.js +148 -47
  103. package/dist/private-local-service-lease.d.ts +16 -0
  104. package/dist/private-local-service-lease.js +105 -0
  105. package/dist/private-local-service-listener.d.ts +9 -2
  106. package/dist/private-local-service-listener.js +260 -64
  107. package/dist/private-local-service-log.d.ts +128 -0
  108. package/dist/private-local-service-log.js +321 -0
  109. package/dist/private-local-service-owner.d.ts +87 -1519
  110. package/dist/private-local-service-owner.js +1069 -71
  111. package/dist/private-local-service-process.js +3 -1
  112. package/dist/private-local-service-termination.d.ts +7 -0
  113. package/dist/private-local-service-termination.js +67 -0
  114. package/dist/private-local-service.d.ts +31 -2
  115. package/dist/private-local-service.js +402 -126
  116. package/dist/private-local-session-discovery.d.ts +52 -5
  117. package/dist/private-local-session-discovery.js +132 -11
  118. package/dist/private-local-shiitake-host.js +2 -2
  119. package/dist/private-local-startup-model.d.ts +51 -0
  120. package/dist/private-local-startup-model.js +86 -0
  121. package/dist/private-local-stop-all.d.ts +27 -0
  122. package/dist/private-local-stop-all.js +26 -0
  123. package/dist/private-local-ui-attachment.d.ts +19 -0
  124. package/dist/private-local-ui-attachment.js +71 -0
  125. package/dist/private-local-worker-server.d.ts +1 -0
  126. package/dist/private-local-worker-server.js +13 -5
  127. package/dist/private-mcp-form-contract.d.ts +186 -0
  128. package/dist/private-mcp-form-contract.js +53 -0
  129. package/dist/private-mcp-form-elicitation.d.ts +18 -0
  130. package/dist/private-mcp-form-elicitation.js +97 -0
  131. package/dist/private-mcp-form-session.d.ts +172 -0
  132. package/dist/private-mcp-form-session.js +148 -0
  133. package/dist/private-openai-claims.d.ts +30 -0
  134. package/dist/private-openai-claims.js +69 -0
  135. package/dist/private-oyster-mcp-auth.d.ts +13 -0
  136. package/dist/private-oyster-mcp-auth.js +57 -0
  137. package/dist/private-provider-rejection.d.ts +15 -0
  138. package/dist/private-provider-rejection.js +24 -0
  139. package/dist/private-sqlite-owner.d.ts +34 -2
  140. package/dist/private-sqlite-owner.js +103 -37
  141. package/dist/private-team-computers.d.ts +4 -4
  142. package/dist/private-team-computers.js +11 -2
  143. package/dist/seatbelt/safe-base.sbpl +79 -0
  144. package/dist/subscription-activation.d.ts +9 -0
  145. package/dist/subscription-activation.js +62 -0
  146. package/dist/subscription-identity.d.ts +23 -0
  147. package/dist/subscription-identity.js +124 -0
  148. package/dist/subscription-turn.d.ts +17 -0
  149. package/dist/subscription-turn.js +143 -0
  150. package/dist/team-apps.d.ts +2 -2
  151. package/dist/team-apps.js +7 -2
  152. package/native/bin/LICENSE.bubblewrap +502 -0
  153. package/native/bin/LICENSE.libcap +112 -0
  154. package/native/bin/LICENSE.sandbox-runtime +201 -0
  155. package/native/bin/SOURCE.bubblewrap.tar.xz +0 -0
  156. package/native/bin/bwrap-build-source/Dockerfile +48 -0
  157. package/native/bin/bwrap-build-source/README.md +96 -0
  158. package/native/bin/bwrap-build-source/build.mjs +52 -0
  159. package/native/bin/bwrap-build-source/linux-arm64.ini +14 -0
  160. package/native/bin/bwrap-build-source/pin.mjs +21 -0
  161. package/native/bin/bwrap-build-source/record.mjs +74 -0
  162. package/native/bin/bwrap-build-source/vendor/LICENSE.sandbox-runtime +201 -0
  163. package/native/bin/bwrap-build-source/vendor/README.md +23 -0
  164. package/native/bin/bwrap-build-source/vendor/seccomp-unix-block.c +280 -0
  165. package/native/bin/bwrap-build.json +44 -0
  166. package/native/bin/fungi-bwrap +0 -0
  167. package/native/bin/fungi-bwrap.sha256 +1 -0
  168. package/native/bin/fungi-seccomp.bpf +0 -0
  169. package/native/bin/fungi-seccomp.bpf.sha256 +1 -0
  170. package/package.json +44 -31
  171. package/dist/private-local-model-settings.d.ts +0 -5
  172. package/dist/private-local-model-settings.js +0 -31
  173. package/dist/private-local-subagents.d.ts +0 -27
  174. package/dist/private-local-subagents.js +0 -77
@@ -0,0 +1,79 @@
1
+ ; Derived from OpenAI Codex, Apache-2.0, Copyright 2025 OpenAI.
2
+ ; codex-rs/sandboxing/src/seatbelt_base_policy.sbpl and seatbelt.rs
3
+ ; rust-v0.162.1 (7a078f6bb4).
4
+ ; Changed by Botanical: no network fragments, PTY/device write grants, shared
5
+ ; memory or power-management grants. Reads and parameterized folder/temp writes
6
+ ; are composed by private-local-seatbelt.ts. Protect .fungi and Git hooks/config,
7
+ ; not all of .git. Denials carry fungi-safe for diagnostic correlation.
8
+ ;
9
+ ; inspired by Chrome's sandbox policy:
10
+ ; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb;l=273-319;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd
11
+ ; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/renderer.sb;l=64;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd
12
+ (version 1)
13
+ (deny default (with message "fungi-safe"))
14
+ ; Automation must not ask an unsandboxed application to act on our behalf.
15
+ (deny appleevent-send (with message "fungi-safe"))
16
+ (allow process-exec)
17
+ (allow process-fork)
18
+ (allow signal (target same-sandbox))
19
+ (allow process-info* (target same-sandbox))
20
+ (allow file-write-data
21
+ (require-all (path "/dev/null") (vnode-type CHARACTER-DEVICE)))
22
+ (allow sysctl-read
23
+ (sysctl-name "hw.activecpu")
24
+ (sysctl-name "hw.busfrequency_compat")
25
+ (sysctl-name "hw.byteorder")
26
+ (sysctl-name "hw.cacheconfig")
27
+ (sysctl-name "hw.cachelinesize_compat")
28
+ (sysctl-name "hw.cpufamily")
29
+ (sysctl-name "hw.cpufrequency_compat")
30
+ (sysctl-name "hw.cputype")
31
+ (sysctl-name "hw.l1dcachesize_compat")
32
+ (sysctl-name "hw.l1icachesize_compat")
33
+ (sysctl-name "hw.l2cachesize_compat")
34
+ (sysctl-name "hw.l3cachesize_compat")
35
+ (sysctl-name "hw.logicalcpu_max")
36
+ (sysctl-name "hw.machine")
37
+ (sysctl-name "hw.model")
38
+ (sysctl-name "hw.memsize")
39
+ (sysctl-name "hw.ncpu")
40
+ (sysctl-name "hw.nperflevels")
41
+ (sysctl-name-prefix "hw.optional.arm.")
42
+ (sysctl-name-prefix "hw.optional.armv8_")
43
+ (sysctl-name "hw.packages")
44
+ (sysctl-name "hw.pagesize_compat")
45
+ (sysctl-name "hw.pagesize")
46
+ (sysctl-name "hw.physicalcpu")
47
+ (sysctl-name "hw.physicalcpu_max")
48
+ (sysctl-name "hw.logicalcpu")
49
+ (sysctl-name "hw.cpufrequency")
50
+ (sysctl-name "hw.tbfrequency_compat")
51
+ (sysctl-name "hw.vectorunit")
52
+ (sysctl-name "machdep.cpu.brand_string")
53
+ (sysctl-name "kern.argmax")
54
+ (sysctl-name "kern.hostname")
55
+ (sysctl-name "kern.maxfilesperproc")
56
+ (sysctl-name "kern.maxproc")
57
+ (sysctl-name "kern.osproductversion")
58
+ (sysctl-name "kern.osrelease")
59
+ (sysctl-name "kern.ostype")
60
+ (sysctl-name "kern.osvariant_status")
61
+ (sysctl-name "kern.osversion")
62
+ (sysctl-name "kern.secure_kernel")
63
+ (sysctl-name "kern.sysv.semmns")
64
+ (sysctl-name "kern.usrstack64")
65
+ (sysctl-name "kern.version")
66
+ (sysctl-name "sysctl.proc_cputype")
67
+ (sysctl-name "vm.loadavg")
68
+ (sysctl-name-prefix "hw.perflevel")
69
+ (sysctl-name-prefix "kern.proc.pgrp.")
70
+ (sysctl-name-prefix "kern.proc.pid.")
71
+ (sysctl-name-prefix "net.routetable."))
72
+ (allow sysctl-write (sysctl-name "kern.grade_cputype"))
73
+ (allow iokit-open (iokit-registry-entry-class "RootDomainUserClient"))
74
+ ; The only Mach service allowed: user lookup used by shell/git (Codex base).
75
+ ; Do not compose Codex's preferences, TLS trust, network or platform fragments:
76
+ ; those grant services which a network-free command does not need.
77
+ ; Needed to look up user info, see https://crbug.com/792228
78
+ (allow mach-lookup (global-name "com.apple.system.opendirectoryd.libinfo"))
79
+ (allow ipc-posix-sem)
@@ -0,0 +1,9 @@
1
+ /** Fixed categories only: neither parser diagnostics nor filesystem causes escape. */
2
+ export declare class CredentialActivationError extends Error {
3
+ readonly category: "invalid-input" | "conflict" | "store-failure" | "identity-unavailable";
4
+ constructor(category: "invalid-input" | "conflict" | "store-failure" | "identity-unavailable");
5
+ }
6
+ /** The credential owner performs a provider-scoped CAS under its refresh lock. */
7
+ export declare function activateSubscriptionCredential(provider: string, input: AsyncIterable<Uint8Array>, options?: {
8
+ credentialPath?: string;
9
+ }): Promise<void>;
@@ -0,0 +1,62 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { z } from "zod";
3
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
4
+ import { createPrivateCredentialStore, oauthCredentialSchema, } from "./private-file-credential-store.js";
5
+ const maximumInputBytes = 256 * 1024;
6
+ const providerSchema = z.enum(["anthropic", "openai"]);
7
+ const subscriptionSchema = oauthCredentialSchema.refine((value) => value.access.length > 0 && value.refresh.length > 0);
8
+ const activationSchema = z.strictObject({
9
+ expected: subscriptionSchema.nullable(),
10
+ credential: subscriptionSchema,
11
+ });
12
+ /** Fixed categories only: neither parser diagnostics nor filesystem causes escape. */
13
+ export class CredentialActivationError extends Error {
14
+ category;
15
+ constructor(category) {
16
+ super(`Credential activation: ${category}`);
17
+ this.category = category;
18
+ this.name = "CredentialActivationError";
19
+ }
20
+ }
21
+ /** The credential owner performs a provider-scoped CAS under its refresh lock. */
22
+ export async function activateSubscriptionCredential(provider, input, options = {}) {
23
+ const providerId = providerSchema.safeParse(provider);
24
+ if (!providerId.success)
25
+ throw new CredentialActivationError("invalid-input");
26
+ const credentialPath = options.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath;
27
+ const chunks = [];
28
+ let size = 0;
29
+ try {
30
+ for await (const chunk of input) {
31
+ size += chunk.byteLength;
32
+ if (size > maximumInputBytes)
33
+ throw new CredentialActivationError("invalid-input");
34
+ chunks.push(chunk);
35
+ }
36
+ }
37
+ catch {
38
+ throw new CredentialActivationError("invalid-input");
39
+ }
40
+ let decoded;
41
+ try {
42
+ decoded = JSON.parse(Buffer.concat(chunks).toString("utf8"));
43
+ }
44
+ catch {
45
+ throw new CredentialActivationError("invalid-input");
46
+ }
47
+ const parsed = activationSchema.safeParse(decoded);
48
+ if (!parsed.success)
49
+ throw new CredentialActivationError("invalid-input");
50
+ try {
51
+ await createPrivateCredentialStore(credentialPath).modify(providerId.data, (current) => {
52
+ if (!isDeepStrictEqual(current ?? null, parsed.data.expected))
53
+ throw new CredentialActivationError("conflict");
54
+ return Promise.resolve(parsed.data.credential);
55
+ });
56
+ }
57
+ catch (error) {
58
+ if (error instanceof CredentialActivationError)
59
+ throw error;
60
+ throw new CredentialActivationError("store-failure");
61
+ }
62
+ }
@@ -0,0 +1,23 @@
1
+ import type { OAuthAuth } from "@earendil-works/pi-ai";
2
+ import type { FetchLike } from "@modelcontextprotocol/client";
3
+ import { type OpenAIIdentityDetail } from "./private-openai-claims.js";
4
+ import { CredentialActivationError } from "./subscription-activation.js";
5
+ /** The sign-in proved no person; carries claim names only, never values. */
6
+ export declare class OpenAIIdentityUnavailable extends CredentialActivationError {
7
+ readonly detail: OpenAIIdentityDetail;
8
+ constructor(detail: OpenAIIdentityDetail);
9
+ }
10
+ /**
11
+ * Pi owns OAuth mechanics. OpenAI identity is only the access token's issuer
12
+ * and subject. Anthropic retains its prepared profile account across refresh.
13
+ */
14
+ export declare function retainSubscriptionIdentity(oauth: OAuthAuth, provider: "anthropic" | "openai"): OAuthAuth;
15
+ /**
16
+ * Explicit import preparation, never an alternate login flow. Anthropic's
17
+ * opaque token requires its own profile. OpenAI's identity is the access
18
+ * token's issuer and subject; preparation only confirms them.
19
+ */
20
+ export declare function prepareSubscriptionIdentity(provider: string, options?: {
21
+ credentialPath?: string;
22
+ fetch?: FetchLike;
23
+ }): Promise<void>;
@@ -0,0 +1,124 @@
1
+ import { z } from "zod";
2
+ import { createPrivateCredentialStore } from "./private-file-credential-store.js";
3
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
4
+ import { readOpenAIPerson, } from "./private-openai-claims.js";
5
+ import { CredentialActivationError } from "./subscription-activation.js";
6
+ /** The sign-in proved no person; carries claim names only, never values. */
7
+ export class OpenAIIdentityUnavailable extends CredentialActivationError {
8
+ detail;
9
+ constructor(detail) {
10
+ super("identity-unavailable");
11
+ this.detail = detail;
12
+ this.name = "OpenAIIdentityUnavailable";
13
+ }
14
+ }
15
+ const identitySchema = z.object({
16
+ accountId: z
17
+ .string()
18
+ .min(1)
19
+ .max(256)
20
+ .refine((value) => value.trim().length > 0),
21
+ });
22
+ const profileSchema = z.object({
23
+ account: z.object({ uuid: z.uuid() }),
24
+ });
25
+ async function readBoundedJson(response, schema) {
26
+ if (!response.ok || response.body === null) {
27
+ await response.body?.cancel();
28
+ throw new CredentialActivationError("identity-unavailable");
29
+ }
30
+ const reader = response.body.getReader();
31
+ const chunks = [];
32
+ let size = 0;
33
+ try {
34
+ for (;;) {
35
+ const chunk = await reader.read();
36
+ if (chunk.done)
37
+ break;
38
+ const value = z.instanceof(Uint8Array).parse(chunk.value);
39
+ size += value.byteLength;
40
+ if (size > 64 * 1024)
41
+ throw new CredentialActivationError("identity-unavailable");
42
+ chunks.push(value);
43
+ }
44
+ }
45
+ finally {
46
+ await reader.cancel();
47
+ reader.releaseLock();
48
+ }
49
+ const parsed = schema.safeParse(JSON.parse(Buffer.concat(chunks).toString("utf8")));
50
+ return parsed.success ? parsed.data : undefined;
51
+ }
52
+ /**
53
+ * Pi owns OAuth mechanics. OpenAI identity is only the access token's issuer
54
+ * and subject. Anthropic retains its prepared profile account across refresh.
55
+ */
56
+ export function retainSubscriptionIdentity(oauth, provider) {
57
+ const retained = {
58
+ ...oauth,
59
+ async refresh(credential, signal) {
60
+ const next = await oauth.refresh(credential, signal);
61
+ const identity = provider === "anthropic"
62
+ ? identitySchema.safeParse(credential)
63
+ : undefined;
64
+ return identity?.success === true
65
+ ? { ...next, accountId: identity.data.accountId }
66
+ : next;
67
+ },
68
+ };
69
+ if (provider === "anthropic")
70
+ return retained;
71
+ return {
72
+ ...retained,
73
+ async login(interaction, loginOptions) {
74
+ const credential = await oauth.login(interaction, loginOptions);
75
+ const person = readOpenAIPerson(credential.access);
76
+ if (person.kind === "unproven")
77
+ throw new OpenAIIdentityUnavailable(person.detail);
78
+ return credential;
79
+ },
80
+ };
81
+ }
82
+ /**
83
+ * Explicit import preparation, never an alternate login flow. Anthropic's
84
+ * opaque token requires its own profile. OpenAI's identity is the access
85
+ * token's issuer and subject; preparation only confirms them.
86
+ */
87
+ export async function prepareSubscriptionIdentity(provider, options = {}) {
88
+ if (provider !== "anthropic" && provider !== "openai")
89
+ throw new CredentialActivationError("invalid-input");
90
+ const store = createPrivateCredentialStore(options.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath);
91
+ try {
92
+ await store.modify(provider, async (current) => {
93
+ if (current?.type !== "oauth")
94
+ throw new CredentialActivationError("invalid-input");
95
+ if (provider === "openai") {
96
+ // The access token's issuer and subject are the whole identity.
97
+ if (readOpenAIPerson(current.access).kind === "unproven")
98
+ throw new CredentialActivationError("identity-unavailable");
99
+ return undefined;
100
+ }
101
+ if (identitySchema.safeParse(current).success)
102
+ return undefined;
103
+ if (current.expires <= Date.now())
104
+ throw new CredentialActivationError("invalid-input");
105
+ const response = await (options.fetch ?? globalThis.fetch)("https://api.anthropic.com/api/oauth/profile", {
106
+ headers: {
107
+ Authorization: `Bearer ${current.access}`,
108
+ "anthropic-beta": "oauth-2025-04-20",
109
+ },
110
+ redirect: "error",
111
+ signal: AbortSignal.timeout(10_000),
112
+ });
113
+ const profile = await readBoundedJson(response, profileSchema);
114
+ if (profile === undefined)
115
+ throw new CredentialActivationError("identity-unavailable");
116
+ return { ...current, accountId: profile.account.uuid };
117
+ });
118
+ }
119
+ catch (error) {
120
+ if (error instanceof CredentialActivationError)
121
+ throw error;
122
+ throw new CredentialActivationError("identity-unavailable");
123
+ }
124
+ }
@@ -0,0 +1,17 @@
1
+ import { type CredentialStore, type Models } from "@earendil-works/pi-ai";
2
+ export declare function registerSubscriptionTurnOwner(models: Models, store: CredentialStore): void;
3
+ declare class SubscriptionTurnError extends Error {
4
+ readonly category = "account-switched-during-turn";
5
+ constructor();
6
+ }
7
+ /** One accepted user turn owns one in-memory credential view, never a new vault. */
8
+ export declare function acquireSubscriptionTurn(models: Models, providerId: string, signal: AbortSignal): Promise<{
9
+ models: Models;
10
+ refusal: () => undefined;
11
+ release: () => Promise<void>;
12
+ } | {
13
+ models: import("@earendil-works/pi-ai").MutableModels;
14
+ refusal: () => SubscriptionTurnError | undefined;
15
+ release: () => Promise<void>;
16
+ }>;
17
+ export {};
@@ -0,0 +1,143 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { createModels, } from "@earendil-works/pi-ai";
3
+ import { z } from "zod";
4
+ import { readOpenAIPerson } from "./private-openai-claims.js";
5
+ // Associate only Local-owned collections with their actual credential owner.
6
+ // Caller-supplied Models retain their own auth contract; never guess a file path.
7
+ const owners = new WeakMap();
8
+ export function registerSubscriptionTurnOwner(models, store) {
9
+ owners.set(models, store);
10
+ }
11
+ class SubscriptionTurnError extends Error {
12
+ category = "account-switched-during-turn";
13
+ constructor() {
14
+ super("Account switched during this turn: retry.");
15
+ this.name = "SubscriptionTurnError";
16
+ }
17
+ }
18
+ const accountSchema = z.object({ accountId: z.string().min(1).max(256) });
19
+ function identity(provider, credential) {
20
+ if (provider === "anthropic") {
21
+ const account = accountSchema.safeParse(credential);
22
+ return account.success ? { person: account.data.accountId } : undefined;
23
+ }
24
+ // Only the access token's issuer and subject identify an OpenAI person.
25
+ try {
26
+ const claims = readOpenAIPerson(credential.access);
27
+ return claims.kind === "unproven"
28
+ ? undefined
29
+ : {
30
+ person: JSON.stringify([claims.iss, claims.sub]),
31
+ };
32
+ }
33
+ catch {
34
+ // Never expose decoder diagnostics or accept an unprovable JWT identity.
35
+ throw new SubscriptionTurnError();
36
+ }
37
+ }
38
+ function sameAccount(provider, current, pin) {
39
+ if (current?.type !== "oauth")
40
+ return false;
41
+ const pinned = identity(provider, pin);
42
+ const now = identity(provider, current);
43
+ // OpenAI always needs a proven person. Anthropic's opaque grant may not yet
44
+ // have its profile account; exact credential equality still proves continuity.
45
+ if (pinned === undefined || now === undefined)
46
+ return provider !== "openai" && isDeepStrictEqual(current, pin);
47
+ return pinned.person === now.person;
48
+ }
49
+ /** One accepted user turn owns one in-memory credential view, never a new vault. */
50
+ export async function acquireSubscriptionTurn(models, providerId, signal) {
51
+ const owner = owners.get(models);
52
+ if (owner === undefined ||
53
+ (providerId !== "anthropic" && providerId !== "openai"))
54
+ return {
55
+ models,
56
+ refusal: () => undefined,
57
+ release: () => Promise.resolve(),
58
+ };
59
+ const provider = models.getProvider(providerId);
60
+ if (provider === undefined)
61
+ throw new Error("Turn provider is not registered");
62
+ let pin = await owner.read(providerId, { signal });
63
+ let released = false;
64
+ let refusal;
65
+ const fail = () => {
66
+ refusal ??= new SubscriptionTurnError();
67
+ throw refusal;
68
+ };
69
+ const assertOpen = () => {
70
+ if (released)
71
+ throw new Error("Model turn already released");
72
+ if (refusal !== undefined)
73
+ throw refusal;
74
+ };
75
+ const adopt = (current) => {
76
+ assertOpen();
77
+ if (pin?.type !== "oauth")
78
+ return pin;
79
+ if (sameAccount(providerId, current, pin))
80
+ pin = current;
81
+ else if (pin.expires <= Date.now())
82
+ fail();
83
+ return pin;
84
+ };
85
+ const view = {
86
+ async read(id, options) {
87
+ assertOpen();
88
+ if (id !== providerId)
89
+ throw new Error("Turn provider changed");
90
+ options?.signal?.throwIfAborted();
91
+ if (pin?.type !== "oauth")
92
+ return pin;
93
+ return adopt(await owner.read(id, options));
94
+ },
95
+ list: () => Promise.resolve(pin === undefined ? [] : [{ providerId, type: pin.type }]),
96
+ async modify(id, callback, options) {
97
+ let resolved;
98
+ await owner.modify(id, async (current) => {
99
+ assertOpen();
100
+ if (id !== providerId || pin?.type !== "oauth")
101
+ throw new Error("Turn OAuth credential unavailable");
102
+ if (!sameAccount(providerId, current, pin)) {
103
+ // Pi may request a proactive refresh while the retired token is still
104
+ // valid. Serve it without invoking refresh or modifying the active B.
105
+ if (pin.expires <= Date.now())
106
+ fail();
107
+ resolved = pin;
108
+ return undefined;
109
+ }
110
+ const next = await callback(current);
111
+ resolved = next ?? current;
112
+ return next;
113
+ }, options);
114
+ // Validate adoption only after the owner commits: cancellation or an
115
+ // unprovable/switched refresh must never discard a rotated refresh token.
116
+ // A released turn must not regain a pin.
117
+ return released ? resolved : adopt(resolved);
118
+ },
119
+ delete: () => Promise.reject(new Error("Turn credentials cannot be deleted")),
120
+ };
121
+ const scoped = createModels({ credentials: view });
122
+ scoped.setProvider(provider);
123
+ try {
124
+ // Pin/refresh at acquisition, not at the first tool iteration.
125
+ if ((await scoped.getAuth(providerId, { signal })) === undefined)
126
+ throw new Error("Turn provider is not signed in");
127
+ }
128
+ catch (error) {
129
+ pin = undefined;
130
+ if (refusal !== undefined)
131
+ throw refusal;
132
+ throw error;
133
+ }
134
+ return {
135
+ models: scoped,
136
+ refusal: () => refusal,
137
+ release: () => {
138
+ released = true;
139
+ pin = undefined;
140
+ return Promise.resolve();
141
+ },
142
+ };
143
+ }
@@ -1,6 +1,6 @@
1
1
  import { type ShiitakeSession } from "@fungi.computer/shiitake/client";
2
2
  import type { FetchLike } from "@modelcontextprotocol/client";
3
- import { type AppDirectory } from "@fungi.computer/oyster/shell/app-directory";
3
+ import { type AppDirectory } from "@fungi.computer/nacre/app-directory";
4
4
  import { type GillRequest, type GillReply } from "@fungi.computer/gills";
5
5
  import { type FungiAccountTeam } from "./private-fungi-account.js";
6
6
  /** A selected Team's App reads. Credentials and selectors remain Local-owned. */
@@ -18,7 +18,7 @@ export type LocalTeamApps = Readonly<{
18
18
  requestGill: (surfaceId: string, selectors: GillRequest, signal?: AbortSignal) => Promise<GillReply>;
19
19
  }>;
20
20
  /** Pin this view to the saved account and Team; a later switch retires it. */
21
- export declare function openLocalTeamApps(options?: Readonly<{
21
+ export declare function openLocalTeamApps(input?: Readonly<{
22
22
  credentialPath?: string;
23
23
  fetch?: FetchLike;
24
24
  }>): Promise<LocalTeamApps>;
package/dist/team-apps.js CHANGED
@@ -1,11 +1,16 @@
1
1
  import { decodeGillRefusal } from "@fungi.computer/agar/gills";
2
2
  import { EventSource } from "eventsource";
3
3
  import { createShiitakeClient, } from "@fungi.computer/shiitake/client";
4
- import { appDirectoryResponseSchema, } from "@fungi.computer/oyster/shell/app-directory";
4
+ import { capturePrivateLocalConfigRoot } from "./private-local-config-root.js";
5
+ import { appDirectoryResponseSchema, } from "@fungi.computer/nacre/app-directory";
5
6
  import { createGillClient, } from "@fungi.computer/gills";
6
7
  import { fungiAccountFetch, readFungiAccountStatus, } from "./private-fungi-account.js";
7
8
  /** Pin this view to the saved account and Team; a later switch retires it. */
8
- export async function openLocalTeamApps(options = {}) {
9
+ export async function openLocalTeamApps(input = {}) {
10
+ const options = {
11
+ ...input,
12
+ credentialPath: input.credentialPath ?? capturePrivateLocalConfigRoot().credentialPath,
13
+ };
9
14
  const account = await readFungiAccountStatus(options);
10
15
  if (account?.team === undefined)
11
16
  throw new Error("Sign in to Fungi and choose a Team to view its Apps");