@fungi.computer/mycelium 0.0.0-stage → 0.0.2

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 (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +303 -2
  3. package/THIRD_PARTY_NOTICES +2 -0
  4. package/dist/access.d.ts +129 -0
  5. package/dist/access.js +166 -0
  6. package/dist/capability-source.d.ts +44 -0
  7. package/dist/capability-source.js +1 -0
  8. package/dist/compiler.d.ts +28 -0
  9. package/dist/compiler.js +221 -0
  10. package/dist/define.d.ts +8 -0
  11. package/dist/define.js +7 -0
  12. package/dist/effect.d.ts +89 -0
  13. package/dist/effect.js +42 -0
  14. package/dist/errors.d.ts +48 -0
  15. package/dist/errors.js +62 -0
  16. package/dist/execute-tool.d.ts +22 -0
  17. package/dist/execute-tool.js +163 -0
  18. package/dist/image-collector.d.ts +47 -0
  19. package/dist/image-collector.js +39 -0
  20. package/dist/index.d.ts +149 -0
  21. package/dist/index.js +22 -0
  22. package/dist/internal/build-identity.d.ts +1 -0
  23. package/dist/internal/build-identity.js +15 -0
  24. package/dist/internal/build-identity.node.d.ts +1 -0
  25. package/dist/internal/build-identity.node.js +42 -0
  26. package/dist/internal/execution-lifecycle.d.ts +44 -0
  27. package/dist/internal/execution-lifecycle.js +328 -0
  28. package/dist/internal/mcp-principal.d.ts +14 -0
  29. package/dist/internal/mcp-principal.js +17 -0
  30. package/dist/mcp-catalog.d.ts +23 -0
  31. package/dist/mcp-catalog.js +110 -0
  32. package/dist/mcp.d.ts +92 -0
  33. package/dist/mcp.js +250 -0
  34. package/dist/operation-schema.d.ts +47 -0
  35. package/dist/operation-schema.js +50 -0
  36. package/dist/promise-facade.d.ts +3 -0
  37. package/dist/promise-facade.js +82 -0
  38. package/dist/registration.d.ts +74 -0
  39. package/dist/registration.js +157 -0
  40. package/dist/remote.d.ts +33 -0
  41. package/dist/remote.js +126 -0
  42. package/dist/runtime.d.ts +51 -0
  43. package/dist/runtime.js +159 -0
  44. package/dist/sandbox-boundary.d.ts +44 -0
  45. package/dist/sandbox-boundary.js +325 -0
  46. package/dist/sandbox-failure-event.d.ts +15 -0
  47. package/dist/sandbox-failure-event.js +34 -0
  48. package/dist/sandbox-failure.d.ts +128 -0
  49. package/dist/sandbox-failure.js +161 -0
  50. package/dist/sandbox-guest.d.ts +34 -0
  51. package/dist/sandbox-guest.js +131 -0
  52. package/dist/sandbox-wire.d.ts +76 -0
  53. package/dist/sandbox-wire.js +190 -0
  54. package/dist/sandbox.d.ts +41 -0
  55. package/dist/sandbox.js +1 -0
  56. package/dist/semaphore-transition.d.ts +7 -0
  57. package/dist/semaphore-transition.js +9 -0
  58. package/dist/source-runtime.d.ts +28 -0
  59. package/dist/source-runtime.js +162 -0
  60. package/dist/testing.d.ts +11 -0
  61. package/dist/testing.js +19 -0
  62. package/dist/workers/index.d.ts +9 -0
  63. package/dist/workers/index.js +90 -0
  64. package/docs/api/README.md +25 -0
  65. package/docs/api/define.md +20 -0
  66. package/docs/api/effect.md +245 -0
  67. package/docs/api/index.md +1081 -0
  68. package/docs/api/mcp.md +239 -0
  69. package/docs/api/remote.md +108 -0
  70. package/docs/api/sandbox-boundary.md +325 -0
  71. package/docs/api/sandbox-failure.md +1133 -0
  72. package/docs/api/sandbox-guest.md +191 -0
  73. package/docs/api/sandbox-wire.md +117 -0
  74. package/docs/api/sandbox.md +81 -0
  75. package/docs/api/semaphore-transition.md +40 -0
  76. package/docs/api/testing.md +72 -0
  77. package/docs/api/workers.md +55 -0
  78. package/package.json +132 -3
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jamstack Consulting LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,304 @@
1
- # Temporary Holding Version
1
+ # Mycelium (`@fungi.computer/mycelium`)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Mycelium gives an Agent typed operations through one `execute` tool. It
4
+ validates operations, acquires a coherent capability snapshot and releases it
5
+ when the run ends. You can also execute a known operation without a model.
6
+
7
+ ## Set up
8
+
9
+ ```sh
10
+ npm install @fungi.computer/mycelium
11
+ ```
12
+
13
+ The package ships ESM only and depends on Zod, Effect, `acorn`,
14
+ [Cairn](../cairn/README.md), [Forage](../forage/README.md), and
15
+ [Mule](../mule/README.md) at runtime, all installed automatically. The root
16
+ entrypoint is the Promise host facade. `@fungi.computer/mycelium/effect`
17
+ supplies the scoped Effect API. Both produce an Effect-native Mule tool. The
18
+ `./sandbox-guest`, `./sandbox-wire`, `./sandbox-failure`, `./sandbox-boundary`,
19
+ and `./semaphore-transition` subpaths support a host's sandbox adapter. A host
20
+ supplies the sandbox that actually evaluates code and enforces isolation,
21
+ external access and cancellation.
22
+
23
+ ## Quick start without a model
24
+
25
+ This example registers `demo.ping`, executes one known binding and prints
26
+ `{"value":"pong"}`. The direct callback deliberately ignores submitted code. It
27
+ is **not isolation**: it runs with host privileges and checks registration,
28
+ validation and cleanup.
29
+
30
+ ```ts
31
+ import { Mycelium } from "@fungi.computer/mycelium";
32
+ import { directSandbox } from "@fungi.computer/mycelium/testing";
33
+ import * as Effect from "effect/Effect";
34
+ import * as Stream from "effect/Stream";
35
+ import { z } from "zod";
36
+
37
+ const runtime = await Mycelium.make({
38
+ modules: [
39
+ Mycelium.module({
40
+ id: "example-demo",
41
+ name: "demo",
42
+ operations: {
43
+ ping: Mycelium.operation({
44
+ input: z.object({}),
45
+ output: z.object({ value: z.string() }),
46
+ execute: async () => ({ value: "pong" }),
47
+ }),
48
+ },
49
+ }),
50
+ ],
51
+ sandbox: directSandbox((bindings, signal) => {
52
+ signal.throwIfAborted();
53
+ const ping = bindings.demo?.ping;
54
+ if (ping === undefined) throw new Error("Missing demo.ping binding");
55
+ return ping({});
56
+ }),
57
+ });
58
+ const lease = await runtime.acquire();
59
+ try {
60
+ const prepared = await Effect.runPromise(
61
+ lease.executeTool.prepare({
62
+ type: "toolCall",
63
+ id: "example-execute",
64
+ name: "execute",
65
+ arguments: { code: "return demo.ping({})" },
66
+ }),
67
+ );
68
+ const parts = await Effect.runPromise(Stream.runCollect(prepared.execute()));
69
+ const text = parts[0]?.result.content[0];
70
+ if (text?.type !== "text" || text.text !== '{"value":"pong"}') {
71
+ throw new Error("Unexpected execute result");
72
+ }
73
+ console.log(text.text);
74
+ } finally {
75
+ await lease.release();
76
+ await runtime.close();
77
+ }
78
+ ```
79
+
80
+ The [public-import fixture](test/stranger/runtime.mjs) uses the same
81
+ known-binding pattern. A real evaluator must implement the public sandbox
82
+ contract. Its request carries code, execution identity, limits and admitted
83
+ bindings. Do not replace it with evaluation in the host's privileged process.
84
+
85
+ ## Operations and discovery
86
+
87
+ Declare input and output with an Effect schema or any Standard Schema that also
88
+ implements Standard JSON Schema, such as Zod 4. Mycelium decodes input with the
89
+ schema before the operation runs. An Effect output schema encodes the result (so
90
+ a codec such as `Schema.NumberFromString` returns `"1"`); a Standard output
91
+ schema validates it. An Effect schema is exchanged as its canonical JSON codec
92
+ (`Schema.toCodecJson`), so the description, the decoder and the encoder agree. A
93
+ throwing filter or transformation is a host error, never reported to the model
94
+ as invalid input. Stable module IDs identify authority. Names become
95
+ model-facing aliases. Input ordering does not choose either identity. Forage
96
+ prepares the bounded discovery catalog for the captured graph.
97
+
98
+ Mule receives this runtime's one top-level `execute` tool. Inner operations,
99
+ including `forage.search` and `forage.describe`, stay behind that tool. The
100
+ operation callback receives only its declared input and execution context.
101
+ Access checks and authorization gates run before the operation. Product
102
+ membership, resource ownership and final external access checks remain host
103
+ responsibilities.
104
+
105
+ ## Access labels and grants
106
+
107
+ Each operation's author labels what it does: `access: "read"` observes,
108
+ `access: "write"` changes something, and an unlabelled operation is `write`. An
109
+ author may add `confirm: true` to ask a person before each call. Labels are
110
+ fixed facts about the operation; the host never edits them.
111
+
112
+ The host passes its decisions down as data in `access`. `grants` maps a module
113
+ id to `"write"` (every operation), `"read"` (only `read` operations) or
114
+ `"none"`. A module the host does not name keeps full access, so a host that
115
+ grants nothing writes no permission code. `sessionGrants` reads the current
116
+ Session's stored grants at every call; they override `grants` key by key, so a
117
+ revoked grant applies to the very next call. Keys in `required` fail closed:
118
+ when neither names one, it is `"none"`. Labels are parsed at definition with
119
+ `ToolPolicySchema`: anything but exactly `"read"`/`"write"` and a boolean
120
+ `confirm` is a definition error. `confirm` is how the host asks a person;
121
+ without it, a `confirm` operation is refused and never runs unasked.
122
+
123
+ ```ts
124
+ const runtime = await Mycelium.make({
125
+ modules: [notes],
126
+ sandbox,
127
+ access: {
128
+ grants: { notes: "read" },
129
+ confirm: (request) => askPerson(request),
130
+ },
131
+ });
132
+ ```
133
+
134
+ Mycelium checks the grant first, then the person, before the operation's input
135
+ decoder runs. A refused call returns an `AccessRefusal` (`module`, `operation`,
136
+ `reason` and a plain `message`) as its ordinary result. `onRefused` observes
137
+ each refusal for logs or metering and never changes the decision.
138
+ `Mycelium.grantAs` looks a module's grant up by a wider owner's key, such as the
139
+ extension that built it. Labelled direct tools passed as `tools` sit behind the
140
+ same check and come back on the lease's `tools`.
141
+ [Access tests](test/access.test.ts) cover each rule.
142
+
143
+ ## Nested namespaces
144
+
145
+ Compose modules in `operations` to expose any namespace depth. The enclosing
146
+ module ID remains the authority identity; child registration IDs do not create
147
+ new authority. For example, a module named `bazaar` with `namespace: ["apps"]`
148
+ and `operations: { windows: windowModule }` exposes the child's `open` operation
149
+ as `env.apps.bazaar.windows.open(...)`. A child's own `namespace` adds segments
150
+ after its registration key. Omit `namespace` for the ordinary depth-1 form
151
+ `env.demo.ping(...)`; the existing `demo.ping(...)` form also works.
152
+
153
+ > An operation key may also be a dotted relative path:
154
+ > `operations: { "repos.list": op }` is called as
155
+ > `env.<module>.repos.list(...)`, exactly like a child module `repos` with
156
+ > operation `list`. Installation refuses a key that is both an operation and a
157
+ > namespace prefix (`files` with `files.read`).
158
+
159
+ Every leaf keeps its declared input/output types. A gate on the enclosing module
160
+ sees relative paths such as `windows.open`. Forage search, list and describe use
161
+ the complete logical path, and descriptions declare the same nested, callable
162
+ `env`. Names are allocated within their logical namespace, so an App and a
163
+ Computer can share a name without renaming either. Transport aliases and flat
164
+ operation tables remain host-only implementation details. Sandbox adapters use
165
+ `/sandbox-guest`'s parsed namespace metadata and `sandboxGuestProgram` to
166
+ project those bindings; provider adapters also use
167
+ `sandboxProviderOperationName` to avoid Codemode's dotted-name collisions.
168
+
169
+ ## Changing capabilities and lifetime
170
+
171
+ Use `sources` when a host must acquire a current Computer, Connection or other
172
+ capability projection. A source returns its bounded snapshot and an awaited
173
+ release function. Its `invalidate()` callback marks that snapshot stale. The
174
+ next lease builds a new coherent graph. An existing lease keeps its captured
175
+ snapshot.
176
+
177
+ Release each lease and close the runtime. Partial acquisition unwinds already
178
+ acquired sources in reverse order. Mycelium owns the scoped resources it
179
+ acquires, while the host owns durable relationships, credentials and current
180
+ policy. A captured snapshot does not excuse a missing final authority check.
181
+
182
+ ## Cancellation and errors
183
+
184
+ Execution has a deadline, a cancellation acknowledgement grace and a bounded
185
+ JSON result. Cancellation aborts the invocation signal and waits for backing
186
+ sandbox settlement. If the sandbox ignores it, the result reports
187
+ `acknowledged: false`. Do not report that work as stopped.
188
+
189
+ The typed failure channel includes syntax errors, guest exceptions, lost
190
+ transport, bad responses, failed capabilities, deadline/cancellation and invalid
191
+ or oversized results. A foreign throw becomes `Unknown`. The model receives the
192
+ bounded `sandboxFailureMessage` projection with a reference. The host receives
193
+ operator evidence through `onSandboxFailure` and the existing reporting path.
194
+ Preserve that evidence without sending host diagnostics to the guest.
195
+
196
+ An Effect host with a Promise sandbox uses `fromTypedSandbox`, which waits for
197
+ that Promise to settle when interrupted. The concrete sandbox and broker decide
198
+ how backing work is stopped. Hosts supply the Worker Loader binding and
199
+ credential custody. The Workers-only `@fungi.computer/mycelium/workers` entry
200
+ exports `createCodemodeSandbox(loader, adapter?, options?)`, a Codemode adapter
201
+ runnable on Workers and celld. It disables guest network access, binds only the
202
+ supplied operations, preserves typed failures and awaits the backing execution
203
+ when cancellation cannot be acknowledged. Import this entry only in Workers
204
+ hosts; the root and other Node entries do not load it.
205
+
206
+ The adapter forwards the request's existing `timeoutMs` budget as the Worker
207
+ Loader's `cpuMs` limit, retaining any tighter loader limit. Cloudflare and celld
208
+ enforce native CPU limits. Local workerd development does not; execution still
209
+ runs. The local host declares that fact with
210
+ `{ localDevUnenforcedLimits: ["cpu"] }` as the third argument. Mycelium prints
211
+ `CPU limits are not enforced in local dev` once per runtime, across sandbox
212
+ instances. Hosts may also declare unenforced `memory`, `output` and `cancel`
213
+ controls; each gets its own once-only diagnostic. The option reports host facts;
214
+ it adds no enforcement or guest policy. Cancellation still waits for backing
215
+ settlement, and a wall-time timeout does not prove isolate termination.
216
+
217
+ The
218
+ [Worker Loader custom-limit contract](https://developers.cloudflare.com/dynamic-workers/usage/limits/)
219
+ and the pinned celld invocation types expose `cpuMs` and `subRequests`. They do
220
+ not expose per-invocation memory or output caps, or an execution-cancel
221
+ operation. The adapter preserves supported loader limits without inventing
222
+ fields for those controls. Host-wide memory limits remain host configuration;
223
+ they are not a guest-widenable sandbox setting. The canonical Mycelium
224
+ invocation owner checks its JSON result budget; calling the bare `Sandbox`
225
+ interface does not run that owner's result validation.
226
+
227
+ An abort before dispatch starts no guest. An abort during execution remains
228
+ authoritative after backing settlement: the adapter's Promise rejects with typed
229
+ `Cancelled { by: "caller" }` instead of resolving a late success. Hosts retain
230
+ invocation custody until that Promise settles; an abort does not release custody
231
+ early. Codemode has no immediate cancellation API. The native qualification
232
+ checks this behavior through the real Worker Loader, along with the trusted
233
+ deadline, CPU forwarding and once-only local notices. These are sandbox checks;
234
+ they do not prove a Workflow host's cancellation path.
235
+
236
+ ## API and examples
237
+
238
+ Use the [generated API reference](docs/api/README.md) and
239
+ [exports](package.json). Root and `/effect` expose lifecycle construction. The
240
+ sandbox subpaths expose wire, guest, failure and boundary contracts for
241
+ adapters.
242
+
243
+ [Capability lifecycle tests](test/capability-plane.test.ts) cover source updates
244
+ and release. [Gate tests](test/gate.test.ts) exercise refusals.
245
+ [Sandbox boundary tests](test/sandbox-boundary.test.ts) exercise failure
246
+ projection. These package proofs do not qualify a particular sandbox's isolation
247
+ or deployment.
248
+
249
+ ## Contribute and license
250
+
251
+ Add a regression to the nearest capability, gate or sandbox test. From the root:
252
+
253
+ ```sh
254
+ pnpm --dir packages/mycelium run test
255
+ pnpm --dir packages/mycelium run docs
256
+ ```
257
+
258
+ MIT. See [LICENSE](LICENSE) and [THIRD_PARTY_NOTICES](THIRD_PARTY_NOTICES).
259
+
260
+ ## MCP
261
+
262
+ `@fungi.computer/mycelium/mcp` exports `createMcp({ catalog, authenticate })`,
263
+ returning `{ handleRequest, close }`. Mount `handleRequest` at the host's MCP
264
+ route. Authentication runs for every HTTP request, including notifications and
265
+ disconnects, and before each operation. Return either
266
+ `{ kind: "admitted", principal }` or `{ kind: "refused", status: 401 | 403 }`.
267
+ The principal is the host's canonical actor: a member with its role
268
+ (`{ actor: { kind: "member", memberId }, role }`), or an Agent or Team key
269
+ (`{ actor: { kind: "agent", agentId } }`,
270
+ `{ actor: { kind: "team-key", keyId } }`). Its ids identify the credential's
271
+ actor, never a bearer token.
272
+
273
+ For installed-App hosts, `createMcpCatalog({ load, sandbox })` adapts ordinary
274
+ Mycelium capability sources. `load(principal)` returns the current
275
+ `{ instructions, sources, grants }` authorized for that principal. It runs again
276
+ before execution and after approval. Each invocation must begin a fresh
277
+ authority read; returning an older cached snapshot cannot establish current
278
+ grants. Grants are explicit: an omitted module is refused. The supplied sandbox
279
+ remains the host's isolated executor; this entry provides no host-fetch
280
+ fallback. This catalog keeps at most eight open connections in process memory
281
+ and releases each acquired tool runtime when its request finishes.
282
+
283
+ Durable Agent hosts implement `McpCatalogProvider` over their existing Session
284
+ owner. `open` atomically reserves the supplied connection bound, `resume`
285
+ returns only an open principal-bound Session, and `refresh` delivers current
286
+ grants. The MCP server independently checks the stored principal. The Session's
287
+ `call` must carry `McpExecutionContext.access` into its ordinary Mycelium
288
+ execution, including direct tools, without persisting request callbacks.
289
+
290
+ MCP execution requires approval for every write-level operation not covered by
291
+ the catalog snapshot's `approvedWrites`, including unlabelled operations. Read
292
+ operations retain their declared confirmation policy. `approvedWrites` names
293
+ granted modules whose writes a person already approved for that principal; it is
294
+ read from the current snapshot at each call, and an operation labelled `confirm`
295
+ still asks. Form elicitation runs before the operation, and authority is
296
+ refreshed again after acceptance; missing or declined approval refuses
297
+ execution. The server supports MCP `2025-11-25` Streamable HTTP so an approval
298
+ can suspend the existing execution without replaying already-completed
299
+ operations. Approval requests contain the module, operation and access label,
300
+ not credential data or operation arguments. `close()` closes transport streams;
301
+ Session retention and connection persistence remain with the catalog owner.
302
+ After transport process loss, a resumed connection has no recorded client
303
+ elicitation capability and refuses writes. Delete that connection and initialize
304
+ a fresh one to negotiate form elicitation again.
@@ -0,0 +1,2 @@
1
+ Mycelium v0.0.1 is informed by the retained MIT-licensed @shiit/mycelium
2
+ implementation. No retained source is copied without an explicit source receipt.
@@ -0,0 +1,129 @@
1
+ import type { MuleTool } from "@fungi.computer/mule/effect";
2
+ import type { SandboxValue } from "./index.js";
3
+ import * as Schema from "effect/Schema";
4
+ /**
5
+ * What one operation does, declared by its author as a fixed fact: `read`
6
+ * observes, `write` changes something. An unlabelled operation is `write`.
7
+ */
8
+ export declare const ToolAccessSchema: Schema.Literals<readonly ["read", "write"]>;
9
+ /** What one operation does: `read` observes, `write` changes something. */
10
+ export type Access = typeof ToolAccessSchema.Type;
11
+ /** Whether the author asks that a person approve each call first. */
12
+ export declare const ToolConfirmSchema: Schema.Boolean;
13
+ /**
14
+ * An author's labels for one operation or direct tool: the one definition
15
+ * both the TypeScript type and the registration-time parse derive from.
16
+ */
17
+ export declare const ToolPolicySchema: Schema.Struct<{
18
+ readonly access: Schema.optional<Schema.Literals<readonly ["read", "write"]>>;
19
+ readonly confirm: Schema.optional<Schema.Boolean>;
20
+ }>;
21
+ /** An author's labels for one operation or direct tool. */
22
+ export type ToolPolicy = typeof ToolPolicySchema.Type;
23
+ /**
24
+ * The access a host grants one module: `write` allows every operation,
25
+ * `read` allows only `read` operations, `none` allows nothing.
26
+ */
27
+ export declare const GrantSchema: Schema.Literals<readonly ["none", "read", "write"]>;
28
+ /** The access a host grants one module. */
29
+ export type Grant = typeof GrantSchema.Type;
30
+ /**
31
+ * A host's grants, keyed by module id. An unnamed module uses defaultGrant
32
+ * unless it is required, in which case missing authority is refused.
33
+ */
34
+ export type Grants = Readonly<Record<string, Grant>>;
35
+ /** The labels an operation or direct tool carries, as its author declared them. */
36
+ export type AccessLabel = Readonly<{
37
+ /** Stable module id the grant is looked up by. */
38
+ module: string;
39
+ /** Operation path inside the module, or the direct tool's name. */
40
+ operation: string;
41
+ access: Access;
42
+ /** The author asks that a person approve each call first. */
43
+ confirm: boolean;
44
+ }>;
45
+ /** The Session a runtime serves; null for a session-free host. */
46
+ export type AccessContext = Readonly<{
47
+ sessionId: string | null;
48
+ }>;
49
+ /** One call the host is asked to approve before it runs. */
50
+ export type ConfirmRequest = Readonly<Omit<AccessLabel, "confirm"> & AccessContext & {
51
+ /** The requested input exactly as the caller supplied it, not yet decoded. */
52
+ input: SandboxValue;
53
+ }>;
54
+ /** Why a call was refused before it ran. */
55
+ export type AccessRefusalReason = "not-granted" | "read-only" | "confirmation-unavailable" | "declined";
56
+ /** The ordinary result a refused call returns in place of running. */
57
+ export type AccessRefusal = Readonly<{
58
+ refused: true;
59
+ module: string;
60
+ operation: string;
61
+ reason: AccessRefusalReason;
62
+ message: string;
63
+ }>;
64
+ /**
65
+ * Host-supplied access data and the one way to ask a person. Every field is
66
+ * optional: hosted defaults allow unnamed modules, and no `confirm` means an
67
+ * operation requiring approval is refused, never run unasked. MCP catalogs
68
+ * explicitly default unnamed modules to none and require approval for writes.
69
+ */
70
+ export type AccessOptions = Readonly<{
71
+ /** Grant for unnamed modules; MCP catalogs use none. Defaults to write for hosted callers. */
72
+ defaultGrant?: Grant;
73
+ /** The default grants as plain data; absent is full access. */
74
+ grants?: Grants;
75
+ /**
76
+ * Module keys under grant control. A required key that neither the Session
77
+ * grants nor the default names is refused, so a missing delivery fails
78
+ * closed. Other unnamed keys use defaultGrant.
79
+ */
80
+ required?: readonly string[];
81
+ /**
82
+ * Read the current Session's stored grants at every call; they override
83
+ * `grants` key by key, so a revoked grant applies to the very next call.
84
+ */
85
+ sessionGrants?: (context: AccessContext) => Grants;
86
+ /** Refresh authority before a call and again after approval. Failure refuses execution. */
87
+ beforeCall?: (signal: AbortSignal) => Promise<void>;
88
+ /** Require approval for every write, including operations without an explicit confirm label. */
89
+ confirmWrites?: boolean;
90
+ /**
91
+ * Read, at every call, the module ids whose writes a person already
92
+ * approved for this caller. Such a write needs no per-call `confirm`; an
93
+ * operation its author labelled `confirm` still asks, and grants still apply.
94
+ */
95
+ approvedWrites?: (context: AccessContext) => readonly string[];
96
+ /** Ask a person to approve one call; resolve true to run it. */
97
+ confirm?: ((request: ConfirmRequest, signal: AbortSignal) => Promise<boolean>) | undefined;
98
+ /** Operator-only observer of every refusal; never changes the decision. */
99
+ onRefused?: (refusal: AccessRefusal, context: AccessContext) => void;
100
+ }>;
101
+ /** A Mule tool that carries its author's access labels outside execute. */
102
+ export type LabelledTool = MuleTool & Readonly<{
103
+ /** Stable module id the grant is looked up by. */
104
+ module: string;
105
+ /** Unlabelled means `write`. */
106
+ access?: Access;
107
+ /** Ask a person before each call. */
108
+ confirm?: boolean;
109
+ }>;
110
+ /**
111
+ * Parse an author's labels at definition. Only the exact `"read"` and
112
+ * `"write"` and a boolean `confirm` are labels; anything else is a definition
113
+ * error, never quietly read as a weaker label.
114
+ */
115
+ export declare function parseAccessLabels(labels: Readonly<{
116
+ access?: SandboxValue;
117
+ confirm?: SandboxValue;
118
+ }>): Pick<AccessLabel, "access" | "confirm">;
119
+ /**
120
+ * Decide one call against its labels and the host's current grants: the grant
121
+ * first, then the person. Returns the refusal, or undefined to run the call.
122
+ */
123
+ export declare function checkAccess(options: AccessOptions, context: AccessContext, label: AccessLabel, input: SandboxValue, signal: AbortSignal): Promise<AccessRefusal | undefined>;
124
+ /**
125
+ * Put one labelled direct tool behind the host's access options. A refused
126
+ * call never reaches the tool's decoder and returns the refusal as its
127
+ * ordinary result, as a refused operation does inside execute.
128
+ */
129
+ export declare function guardTool(tool: LabelledTool, options: AccessOptions, context: AccessContext): MuleTool;
package/dist/access.js ADDED
@@ -0,0 +1,166 @@
1
+ import { MyceliumDefinitionError } from "./errors.js";
2
+ import * as Effect from "effect/Effect";
3
+ import * as Result from "effect/Result";
4
+ import * as Schema from "effect/Schema";
5
+ import * as Stream from "effect/Stream";
6
+ /**
7
+ * What one operation does, declared by its author as a fixed fact: `read`
8
+ * observes, `write` changes something. An unlabelled operation is `write`.
9
+ */
10
+ export const ToolAccessSchema = Schema.Literals(["read", "write"]);
11
+ /** Whether the author asks that a person approve each call first. */
12
+ export const ToolConfirmSchema = Schema.Boolean;
13
+ /**
14
+ * An author's labels for one operation or direct tool: the one definition
15
+ * both the TypeScript type and the registration-time parse derive from.
16
+ */
17
+ export const ToolPolicySchema = Schema.Struct({
18
+ access: Schema.optional(ToolAccessSchema),
19
+ confirm: Schema.optional(ToolConfirmSchema),
20
+ });
21
+ /**
22
+ * The access a host grants one module: `write` allows every operation,
23
+ * `read` allows only `read` operations, `none` allows nothing.
24
+ */
25
+ export const GrantSchema = Schema.Literals(["none", "read", "write"]);
26
+ const decodeToolPolicy = Schema.decodeUnknownResult(ToolPolicySchema);
27
+ const decodeGrant = Schema.decodeUnknownResult(GrantSchema);
28
+ /**
29
+ * Parse an author's labels at definition. Only the exact `"read"` and
30
+ * `"write"` and a boolean `confirm` are labels; anything else is a definition
31
+ * error, never quietly read as a weaker label.
32
+ */
33
+ export function parseAccessLabels(labels) {
34
+ const parsed = decodeToolPolicy({
35
+ access: labels.access,
36
+ confirm: labels.confirm,
37
+ });
38
+ if (Result.isFailure(parsed))
39
+ throw new MyceliumDefinitionError('Access labels must be access: "read" | "write" and confirm: boolean');
40
+ return {
41
+ access: parsed.success.access ?? "write",
42
+ confirm: parsed.success.confirm ?? false,
43
+ };
44
+ }
45
+ function messageFor(reason, label) {
46
+ const name = `${label.module}.${label.operation}`;
47
+ switch (reason) {
48
+ case "not-granted":
49
+ return `${name} is not granted to this Agent.`;
50
+ case "read-only":
51
+ return `${name} changes something, and this Agent has read-only access to ${label.module}.`;
52
+ case "confirmation-unavailable":
53
+ return `${name} needs a person's approval, and no one can be asked here.`;
54
+ case "declined":
55
+ return `${name} was not approved.`;
56
+ }
57
+ }
58
+ function refusal(reason, label) {
59
+ return Object.freeze({
60
+ refused: true,
61
+ module: label.module,
62
+ operation: label.operation,
63
+ reason,
64
+ message: messageFor(reason, label),
65
+ });
66
+ }
67
+ /**
68
+ * The grant for one module. An unnamed module uses the configured default
69
+ * unless it is required; a present entry that is not a grant refuses, so a
70
+ * malformed host value fails closed.
71
+ */
72
+ function grantFor(options, context, module) {
73
+ const session = options.sessionGrants?.(context);
74
+ const owner = session !== undefined && Object.hasOwn(session, module)
75
+ ? session
76
+ : options.grants;
77
+ if (owner === undefined || !Object.hasOwn(owner, module))
78
+ return options.required?.includes(module) === true
79
+ ? "none"
80
+ : (options.defaultGrant ?? "write");
81
+ const parsed = decodeGrant(owner[module]);
82
+ return Result.isSuccess(parsed) ? parsed.success : "none";
83
+ }
84
+ /**
85
+ * Decide one call against its labels and the host's current grants: the grant
86
+ * first, then the person. Returns the refusal, or undefined to run the call.
87
+ */
88
+ export async function checkAccess(options, context, label, input, signal) {
89
+ const decided = await decide(options, context, label, input, signal);
90
+ if (decided !== undefined)
91
+ options.onRefused?.(decided, context);
92
+ return decided;
93
+ }
94
+ async function decide(options, context, label, input, signal) {
95
+ await options.beforeCall?.(signal);
96
+ signal.throwIfAborted();
97
+ const granted = grantRefusal(options, context, label);
98
+ if (granted !== undefined)
99
+ return granted;
100
+ if (!label.confirm &&
101
+ !(options.confirmWrites === true && label.access !== "read"))
102
+ return undefined;
103
+ if (!label.confirm &&
104
+ options.approvedWrites?.(context).includes(label.module) === true)
105
+ return undefined;
106
+ if (options.confirm === undefined)
107
+ return refusal("confirmation-unavailable", label);
108
+ const approved = await options.confirm({
109
+ module: label.module,
110
+ operation: label.operation,
111
+ access: label.access,
112
+ sessionId: context.sessionId,
113
+ input,
114
+ }, signal);
115
+ if (!approved)
116
+ return refusal("declined", label);
117
+ await options.beforeCall?.(signal);
118
+ signal.throwIfAborted();
119
+ // A grant revoked while the person was deciding still applies.
120
+ return grantRefusal(options, context, label);
121
+ }
122
+ /** The refusal the current grant gives this label, if any. */
123
+ function grantRefusal(options, context, label) {
124
+ const grant = grantFor(options, context, label.module);
125
+ if (grant === "none")
126
+ return refusal("not-granted", label);
127
+ // Only an exact "read" label is a read; every other operation writes.
128
+ if (grant === "read" && label.access !== "read")
129
+ return refusal("read-only", label);
130
+ return undefined;
131
+ }
132
+ /**
133
+ * Put one labelled direct tool behind the host's access options. A refused
134
+ * call never reaches the tool's decoder and returns the refusal as its
135
+ * ordinary result, as a refused operation does inside execute.
136
+ */
137
+ export function guardTool(tool, options, context) {
138
+ const label = {
139
+ module: tool.module,
140
+ operation: tool.descriptor.name,
141
+ ...parseAccessLabels(tool),
142
+ };
143
+ return {
144
+ ...tool,
145
+ prepare: (call) => Effect.tryPromise({
146
+ try: (signal) => checkAccess(options, context, label, call.arguments, signal),
147
+ // Host failures stay out of the model's view; the call fails closed.
148
+ catch: () => ({ message: "Access check failed" }),
149
+ }).pipe(Effect.flatMap((refused) => refused === undefined
150
+ ? tool.prepare(call)
151
+ : Effect.succeed({
152
+ execute: () => Stream.succeed({
153
+ type: "result",
154
+ result: {
155
+ content: [
156
+ {
157
+ type: "text",
158
+ text: JSON.stringify(refused),
159
+ },
160
+ ],
161
+ isError: false,
162
+ },
163
+ }),
164
+ }))),
165
+ };
166
+ }
@@ -0,0 +1,44 @@
1
+ import type * as Schema from "effect/Schema";
2
+ import type { ModuleRegistration } from "./registration.js";
3
+ /** Session on whose behalf one runtime acquires its capability projections. */
4
+ export type CapabilitySession = Readonly<{
5
+ /** Stable Session identity supplied by the session host (Shiitake). */
6
+ sessionId: string;
7
+ /** Host's stored command identity; stable across run recovery. */
8
+ commandId?: string;
9
+ /** Opaque host metadata from that stored command, never a model argument. */
10
+ meta?: typeof Schema.Json.Type;
11
+ }>;
12
+ /** Context supplied while acquiring one current capability projection. */
13
+ export type CapabilitySourceContext = Readonly<{
14
+ /**
15
+ * Session the projection is acquired for; absent for session-free hosts.
16
+ * A source uses it to project only what that Session is permitted to use.
17
+ */
18
+ session?: CapabilitySession;
19
+ /** Cooperative cancellation for this bounded acquisition attempt. */
20
+ signal: AbortSignal;
21
+ /** Mark the candidate stale when the source's current projection changes. */
22
+ invalidate: () => void;
23
+ }>;
24
+ /** Bounded current projection returned by one capability source. */
25
+ export type CapabilitySourceSnapshot = Readonly<{
26
+ /** Stable, secret-free version of this source projection. */
27
+ version: string;
28
+ /** Unordered modules contributed by this projection. */
29
+ modules: readonly ModuleRegistration[];
30
+ }>;
31
+ /** Acquired source projection and the cleanup that owns its live adapter. */
32
+ export type CapabilitySourceAcquisition = Readonly<{
33
+ /** Current immutable projection captured by this acquisition. */
34
+ snapshot: CapabilitySourceSnapshot;
35
+ /** Release subscriptions and adapters owned by this acquisition. */
36
+ release: () => Promise<void>;
37
+ }>;
38
+ /** Promise-shaped host boundary for one versioned capability projection. */
39
+ export type CapabilitySource = Readonly<{
40
+ /** Stable, bounded, secret-free source identity. */
41
+ id: string;
42
+ /** Acquire one current projection and its awaited cleanup. */
43
+ acquire: (context: CapabilitySourceContext) => Promise<CapabilitySourceAcquisition>;
44
+ }>;
@@ -0,0 +1 @@
1
+ export {};