@fungi.computer/computer 0.0.0-stage → 0.0.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 (91) hide show
  1. package/Dockerfile +85 -0
  2. package/LICENSE +21 -0
  3. package/README.md +228 -2
  4. package/container/computer-init.mjs +119 -0
  5. package/container/computer-start.sh +24 -0
  6. package/container/package.json +14 -0
  7. package/container/pnpm-lock.yaml +378 -0
  8. package/container/pnpm-workspace.yaml +5 -0
  9. package/container/resource-bounds.mjs +54 -0
  10. package/container/runtime-privileges.cc +37 -0
  11. package/container/runtime-privileges.mjs +11 -0
  12. package/container/terminal-assets/README.md +35 -0
  13. package/container/terminal-assets/bashrc +49 -0
  14. package/container/terminal-assets/git-prompt.sh +586 -0
  15. package/container/terminal-protocol.mjs +58 -0
  16. package/container/terminal-runtime.mjs +10 -0
  17. package/container/terminal-server.mjs +243 -0
  18. package/dist/apply-patch/LICENSE.codex +201 -0
  19. package/dist/apply-patch/build.mjs +98 -0
  20. package/dist/apply-patch/cache.mjs +168 -0
  21. package/dist/apply-patch/compile.mjs +199 -0
  22. package/dist/apply-patch/docker/darwin.Dockerfile +15 -0
  23. package/dist/apply-patch/docker/linux-arm64.Dockerfile +38 -0
  24. package/dist/apply-patch/docker/linux-x64.Dockerfile +23 -0
  25. package/dist/apply-patch/main.rs +320 -0
  26. package/dist/apply-patch/notices.mjs +44 -0
  27. package/dist/apply-patch/pin.mjs +8 -0
  28. package/dist/apply-patch/toolchain.mjs +39 -0
  29. package/dist/computer-backends.d.ts +94 -0
  30. package/dist/computer-backends.js +103 -0
  31. package/dist/computerd.cjs +18010 -0
  32. package/dist/container-policy.d.ts +7 -0
  33. package/dist/container-policy.js +24 -0
  34. package/dist/container-system-trust.d.ts +8 -0
  35. package/dist/container-system-trust.js +66 -0
  36. package/dist/egress-policy.d.ts +89 -0
  37. package/dist/egress-policy.js +240 -0
  38. package/dist/execution-result.d.ts +3 -0
  39. package/dist/execution-result.js +28 -0
  40. package/dist/files-patch.d.ts +20 -0
  41. package/dist/files-patch.js +108 -0
  42. package/dist/files.d.ts +75 -0
  43. package/dist/files.js +136 -0
  44. package/dist/fleet/admission.d.ts +14 -0
  45. package/dist/fleet/admission.js +10 -0
  46. package/dist/fleet/backing.d.ts +52 -0
  47. package/dist/fleet/backing.js +242 -0
  48. package/dist/fleet/computers.d.ts +62 -0
  49. package/dist/fleet/computers.js +35 -0
  50. package/dist/fleet/index.d.ts +11 -0
  51. package/dist/fleet/index.js +11 -0
  52. package/dist/fleet/inventory-wire.d.ts +29 -0
  53. package/dist/fleet/inventory-wire.js +15 -0
  54. package/dist/fleet/inventory.d.ts +24 -0
  55. package/dist/fleet/inventory.js +152 -0
  56. package/dist/fleet/lifecycle.d.ts +99 -0
  57. package/dist/fleet/lifecycle.js +423 -0
  58. package/dist/fleet/runtime-usage.d.ts +50 -0
  59. package/dist/fleet/runtime-usage.js +145 -0
  60. package/dist/fleet/slots.d.ts +30 -0
  61. package/dist/fleet/slots.js +46 -0
  62. package/dist/fleet/wire.d.ts +228 -0
  63. package/dist/fleet/wire.js +127 -0
  64. package/dist/host-config.d.ts +12 -0
  65. package/dist/host-config.js +10 -0
  66. package/dist/index.d.ts +282 -0
  67. package/dist/index.js +1237 -0
  68. package/dist/light-shell.d.ts +44 -0
  69. package/dist/light-shell.js +376 -0
  70. package/dist/local-backend.d.ts +52 -0
  71. package/dist/local-backend.js +143 -0
  72. package/dist/metered-container-host.d.ts +26 -0
  73. package/dist/metered-container-host.js +48 -0
  74. package/dist/on-box-vm.d.ts +119 -0
  75. package/dist/on-box-vm.js +294 -0
  76. package/dist/operation-contract.d.ts +456 -0
  77. package/dist/operation-contract.js +452 -0
  78. package/dist/operation-record.d.ts +50 -0
  79. package/dist/operation-record.js +162 -0
  80. package/dist/preview.d.ts +55 -0
  81. package/dist/preview.js +151 -0
  82. package/dist/process-inventory.d.ts +80 -0
  83. package/dist/process-inventory.js +427 -0
  84. package/dist/pull-pending.d.ts +26 -0
  85. package/dist/pull-pending.js +35 -0
  86. package/dist/team-candidate-builder.mjs +28170 -0
  87. package/dist/transport-backend.d.ts +3 -0
  88. package/dist/transport-backend.js +99 -0
  89. package/dist/workspace-stream.d.ts +2 -0
  90. package/dist/workspace-stream.js +31 -0
  91. package/package.json +126 -3
package/Dockerfile ADDED
@@ -0,0 +1,85 @@
1
+ # node-pty ships no Linux prebuild; compile it against the runtime's Node ABI.
2
+ FROM node:24.20.0-trixie-slim@sha256:a747ad80c8a161b650d79a6da9c422005b91148b18b8d2c669eb5a0b7c07e600 AS terminal
3
+ RUN apt-get update \
4
+ && apt-get install -y --no-install-recommends python3 make g++ pkg-config libfuse-dev \
5
+ && rm -rf /var/lib/apt/lists/*
6
+ RUN npm install --global --no-fund --no-audit pnpm@11.24.0
7
+ WORKDIR /opt/fungi
8
+ COPY container/package.json container/pnpm-lock.yaml container/pnpm-workspace.yaml ./
9
+ RUN pnpm install --frozen-lockfile --prod --config.node-linker=hoisted \
10
+ && cd node_modules/node-pty \
11
+ && mv build/Release/pty.node /tmp/pty.node \
12
+ && rm -rf build deps third_party src scripts typings prebuilds node-addon-api \
13
+ && mkdir -p build/Release \
14
+ && mv /tmp/pty.node build/Release/pty.node
15
+
16
+ COPY container/runtime-privileges.cc ./
17
+ RUN g++ -shared -fPIC -I/usr/local/include/node runtime-privileges.cc -o runtime-privileges.node
18
+
19
+ # Compile Cordyceps' native apply-patch engine (source copied into
20
+ # dist/apply-patch by this package's build) for this image's own platform.
21
+ FROM node:24.20.0-trixie-slim@sha256:a747ad80c8a161b650d79a6da9c422005b91148b18b8d2c669eb5a0b7c07e600 AS apply-patch
22
+ RUN apt-get update \
23
+ && apt-get install -y --no-install-recommends \
24
+ build-essential ca-certificates curl pkg-config libssl-dev \
25
+ && rm -rf /var/lib/apt/lists/*
26
+ ENV RUSTUP_HOME=/opt/rustup CARGO_HOME=/opt/cargo
27
+ ENV PATH=/opt/cargo/bin:$PATH
28
+ COPY dist/apply-patch/pin.mjs /source/
29
+ RUN toolchain="$(node --input-type=module --eval "import { codexPatchPin } from '/source/pin.mjs'; console.log(codexPatchPin.rustToolchain)")" \
30
+ && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup.sh \
31
+ && sh /tmp/rustup.sh -y --profile minimal --default-toolchain "$toolchain" \
32
+ && rm /tmp/rustup.sh
33
+ COPY dist/apply-patch/ /source/
34
+ RUN node /source/compile.mjs --out /out --scratch /scratch
35
+
36
+ # Match the repository toolchain on the same Linux x64 target as computerd.
37
+ FROM node:24.20.0-trixie-slim@sha256:a747ad80c8a161b650d79a6da9c422005b91148b18b8d2c669eb5a0b7c07e600
38
+
39
+ RUN apt-get update \
40
+ && apt-get install -y --no-install-recommends \
41
+ fuse3 libfuse2t64 ca-certificates curl git tini \
42
+ && rm -rf /var/lib/apt/lists/*
43
+
44
+ RUN npm install --global --no-fund --no-audit pnpm@11.24.0 \
45
+ && test "$(node --version)" = "v24.20.0" \
46
+ && test "$(pnpm --version)" = "11.24.0"
47
+
48
+ # Build contexts may come from private gate checkouts (umask 077). Runtime
49
+ # modules must remain readable after init drops to the Computer's own UID.
50
+ # dist/computerd.cjs comes from this package's own build, which copies it from
51
+ # @fungi.computer/cordyceps; the apply-patch engine and its crate notices come
52
+ # from the builder stage above. The image assembly step
53
+ # (scripts/assemble-image.mjs, also run by prepack) copies Oyster's App
54
+ # candidate builder into dist/ before this Dockerfile is built.
55
+ COPY dist/computerd.cjs dist/team-candidate-builder.mjs /opt/fungi/
56
+ COPY --from=apply-patch --chmod=755 /out/fungi-apply-patch /opt/fungi/fungi-apply-patch
57
+ COPY --from=apply-patch /out/build.json /opt/fungi/fungi-apply-patch.notices.json
58
+ COPY dist/apply-patch/LICENSE.codex /opt/fungi/fungi-apply-patch.LICENSE
59
+ COPY --from=terminal /opt/fungi/node_modules /opt/fungi/node_modules
60
+ COPY container/runtime-privileges.mjs /opt/fungi/
61
+ COPY --from=terminal /opt/fungi/runtime-privileges.node /opt/fungi/
62
+ COPY container/package.json container/computer-init.mjs container/computer-start.sh container/resource-bounds.mjs container/terminal-server.mjs container/terminal-runtime.mjs container/terminal-protocol.mjs /opt/fungi/
63
+ COPY container/terminal-assets/bashrc container/terminal-assets/git-prompt.sh /opt/fungi/terminal-assets/
64
+ RUN chmod -R a+rX /opt/fungi \
65
+ && printf '\n. /opt/fungi/terminal-assets/bashrc\n' >> /etc/bash.bashrc \
66
+ && printf '. /opt/fungi/terminal-assets/bashrc\n' > /etc/profile.d/fungi-terminal.sh
67
+
68
+ # No customer process can invoke a setuid/setgid helper, including fusermount.
69
+ # FUSE is mounted while init is root, before either runtime listener opens.
70
+ RUN find / -xdev -perm /6000 -exec chmod a-s {} +
71
+
72
+ ENV PORT=8080
73
+ ENV MOUNT_POINT=/workspace
74
+ ENV FUSE_MOUNT=auto
75
+ ENV COMPUTER_TERMINAL_PORT=7681
76
+ ENV NODE_EXTRA_CA_CERTS=/etc/cloudflare/certs/cloudflare-containers-ca.crt
77
+ ENV SSL_CERT_FILE=/tmp/fungi-ca-bundle.crt
78
+ ENV GIT_SSL_CAINFO=/tmp/fungi-ca-bundle.crt
79
+ EXPOSE 8080 7681
80
+
81
+ # tini is PID 1 and only reaps orphans and forwards signals. computer-init is
82
+ # the one supervisor: it owns computerd and the terminal server, and exits
83
+ # with them so the Containers runtime replaces the whole Computer runtime.
84
+ ENTRYPOINT ["/usr/bin/tini", "-g", "--"]
85
+ CMD ["/usr/bin/bash", "/opt/fungi/computer-start.sh"]
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,229 @@
1
- # Temporary Holding Version
1
+ # Computer runtime
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
+ This private runtime hosts one logical Fungi Computer: a SQLite-backed workspace
4
+ of files, admitted operations, and optional execution backends. The Hub composes
5
+ it with Team authorization, capacity and billing. The runtime is not an
6
+ independently installed npm VM service and does not own Team roles.
7
+
8
+ ## Fleet contracts
9
+
10
+ Node tooling and browser consumers can import the grant/fact schemas and types
11
+ from `@fungi.computer/computer/fleet/wire`, and backing descriptions from
12
+ `@fungi.computer/computer/backing`. These entries do not load Worker runtime
13
+ declarations. The `./fleet` lifecycle and main Computer entry require the host
14
+ runtime's Cloudflare Workers types.
15
+
16
+ `@fungi.computer/computer/fleet` owns the host-neutral grant check, backing
17
+ records and fact contracts. A host authenticates its caller and supplies a
18
+ `ComputerGrant`: one Computer, explicit actions, an expiry, optional host-owned
19
+ egress data, plus opaque actor/invocation provenance. `checkComputerGrant`
20
+ returns `admitted` or `Refused`; grants impose no paid-usage budget. A caller's
21
+ per-command timeout remains a command parameter enforced by the operation
22
+ executor. Hub credit enforcement remains on its existing funded/unfunded
23
+ admission and `creditChanged` → credit-publication restate/stop path.
24
+
25
+ `createFleetBacking` accepts the maintained SQLite owner. Creation records a
26
+ plain backing decision once. Explicit fleet-default and Computer overrides
27
+ remain possible; local attachment keeps replay identity, actor ownership and
28
+ detach cancellation. Mutations return complete backend projections rather than
29
+ calling a host or delivering messages. The composing host can include the call
30
+ and durable outbox staging in its SQLite transaction. Its deployment chooses the
31
+ offered backing kinds; the fleet never rereads deployment configuration.
32
+
33
+ `ComputerFact` describes process exit, file writes, egress, lifecycle and
34
+ runtime interval observations with provenance. CPU time is nullable when
35
+ unmeasured. The host reads these records and owns settlement and delivery.
36
+ Host-specific egress data is decoded by its host; the fleet contract carries
37
+ JSON data without importing the host's policy.
38
+
39
+ The fleet also owns inventory/replay receipts, running-slot storage, runtime
40
+ intervals, and native lifecycle activity/recycling. Inventory admission stays at
41
+ the host: an existing create receipt is checked before funding a new create.
42
+ Slot capacity is explicit infrastructure input; grants carry no usage cap.
43
+ `FleetLifecycle.run` checks a host-issued start grant before native execution.
44
+ The host consumes returned reservation/diagnostic facts and
45
+ `ComputerRuntimeUsage.pendingFacts` interval facts; it owns credit changes,
46
+ usage delivery, logging, and Team Git. Acknowledgment retains the existing
47
+ runtime checkpoint identity and occurs only after delivery. Transport activity,
48
+ idle sleep, and VM recycle sequencing retain the existing runtime behavior. The
49
+ Hub records creation backing facts for its declared known Computers at
50
+ initialization; the fleet never derives missing records on reads.
51
+
52
+ The Hub projects each current resource decision into a grant for the requested
53
+ Files or execution permission. It carries the stored decision's expiry and
54
+ actor/attachment identity. Native acceptance still rechecks the current Hub
55
+ decision before fleet admission; a grant is never cached across that check. The
56
+ Hub projects interval facts into its existing usage Post payload without
57
+ changing billing or credit-stop behavior.
58
+
59
+ The repository architecture test rejects upward imports from this runtime into
60
+ Apps, Billing, Broker or Team/plan modules, including type and dynamic imports.
61
+
62
+ ## Source setup
63
+
64
+ Use a configured repository checkout with the pinned Node and pnpm tools. Run
65
+ from the repository root:
66
+
67
+ ```sh
68
+ pnpm turbo:run run build --filter=@fungi.computer/computer
69
+ pnpm --filter @fungi.computer/computer typecheck
70
+ pnpm --filter @fungi.computer/computer lint
71
+ pnpm --filter @fungi.computer/computer test
72
+ ```
73
+
74
+ The build emits the runtime and copies its daemon/build assets. The test suite
75
+ uses disposable Workers and proof containers, so container proofs require a
76
+ working local Docker/Workers environment. These commands do not deploy a Hub,
77
+ publish an App or replace a production Computer image.
78
+
79
+ ## Files and operation results
80
+
81
+ [ComputerDO](src/index.ts) composes the maintained Cloudflare Workspace and
82
+ SQLite provider. Files belong to that workspace, not to a running shell process.
83
+ A file read can work with no available execution backend.
84
+
85
+ [Operation contracts](src/operation-contract.ts) declare discovery, bounded file
86
+ reads/writes, directories, command execution, processes and port exposure. The
87
+ [Files adapter](src/files.ts) implements the same read and compare/replace
88
+ contract for Agent tools. Failed containment, conflicting contents and reserved
89
+ ports remain explicit errors.
90
+
91
+ An execution request carries a replay key. The
92
+ [operation record](src/operation-record.ts) admits the immutable input and
93
+ retains its result. Repeating that key reads the same operation rather than
94
+ silently starting it again with different content. Accepted, running, settled
95
+ and uncertain outcomes are distinct. An uncertain result is not a successful
96
+ command or proof that no side effect happened.
97
+
98
+ The Workspace is authoritative. A Files save pushes to a hosted runtime that is
99
+ already running, and a Files read pulls from it first. While a terminal is
100
+ attached, the Computer pushes and then pulls every 3 s, so the open terminal and
101
+ Files stay close to each other without either side waiting on the other. A
102
+ stopped runtime is never woken to sync; it catches up through its own push/pull
103
+ bracket on its next command or terminal attach. A failed sync stays pending in
104
+ the library's own cursors and is resent on the next sync, so it never fails the
105
+ Files operation that triggered it. There is no merge: each path takes the last
106
+ version applied. Edits to the same path on both sides between two syncs resolve
107
+ to the Workspace's version, because the loop pushes before it pulls. A
108
+ compare-and-replace save pulls first, so a newer terminal edit surfaces as an
109
+ honest conflict instead of a silent overwrite, while a blind write still
110
+ overwrites the container's copy.
111
+
112
+ ## Choose an execution backend
113
+
114
+ [Backend routing](src/computer-backends.ts) owns these choices:
115
+
116
+ | Operation | Routing |
117
+ | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
118
+ | `exec` without a backend ID | Built-in Worker shell. |
119
+ | `bash`, or execution naming a backend | The named backend, or online local folders before on-box VMs before platform containers. Attach order breaks ties. |
120
+ | Terminal, processes and ports | Hosted backends only. A local folder cannot serve these operations. |
121
+
122
+ An explicit unavailable backend never silently falls back. With no usable
123
+ backend, execution returns `no_backend` or `backend_unavailable`; workspace
124
+ Files remain separate. The backend list is bounded and IDs are unique.
125
+
126
+ [Local backends](src/local-backend.ts) connect through the member's device-key
127
+ admission and dial-out socket. Files sync with the attached folder. Commands run
128
+ with that member's OS access. [On-box VM support](src/on-box-vm.ts) adapts a
129
+ registered host to the Workspace backend interface. Platform containers use the
130
+ maintained Cloudflare container backend. Neither backend label nor a green
131
+ container fixture certifies an installed VM host or production routing.
132
+
133
+ ## Processes, ports and lifetime
134
+
135
+ Start requires a host-owned opaque `requestId` and `requestScope`, plus a
136
+ command, optional working directory and optional declared port. Within this
137
+ Computer, the same scope/key and immutable input return the retained process;
138
+ changed input conflicts. Replay protection lasts while its process record is
139
+ retained (at most 64 records, with 16 running **per host scope**). Another scope
140
+ cannot consume these slots or evict its replay protection. Once this scope's own
141
+ history evicts an exited record, its key can start a new process; callers must
142
+ not retry a retired command as new work. Inventory presentation lists the newest
143
+ 64 records across the Computer; retained records can outlive that read window. A
144
+ retained replay never starts another command or boots a backend. A running
145
+ receipt replays as `uncertain`; liveness reads verify execution. Failed dispatch
146
+ retains an exited receipt.
147
+
148
+ App hosts derive scope from Installation, method and member/actor, separately
149
+ from the unchanged opaque invocation key. The helper uses an unambiguous JSON
150
+ tuple and labels the caller `member:<id>`, `agent:<id>` or `team-key:<id>`. One
151
+ method invocation permits one declared start: identical input is a retry,
152
+ different input conflicts. The trusted App HTTP route takes only the sealed host
153
+ method label and derives Installation and member from its admitted principal;
154
+ arbitrary requestScope is invalid. Native model capability input has only
155
+ command/cwd/port; public call mints its own native-tool scope/key. Scope strings
156
+ are replay identities, not grants.
157
+
158
+ Admission records the process and any newly created port exposure in one SQLite
159
+ transaction. Process rows carry `port` (`null` when none was declared). An
160
+ observed exit or refused dispatch removes only that process's exposure; manual
161
+ exposures remain, including an explicit manual re-expose. Request/frame
162
+ cancellation after host dispatch does not cancel Computer-owned work.
163
+
164
+ [Process inventory](src/process-inventory.ts) retains owner-issued identities
165
+ and reports running, exited or uncertain state. Worker reconstruction and
166
+ backend termination are different events. A reachable backend can preserve a
167
+ process across Worker reconstruction; a sleeping or stopped backend does not
168
+ keep that process alive.
169
+
170
+ Ordinary command execution is bounded to two minutes. Long-running work uses
171
+ process operations. Port exposure is explicit and bounded. Ports 8080 and 7681
172
+ belong to Computer control/terminal daemons and cannot be application previews.
173
+ The Hub owns preview admission and URLs; this runtime does not expose arbitrary
174
+ Computer control endpoints to the browser.
175
+
176
+ [Container policy](src/container-policy.ts) configures idle sleep and respects
177
+ `HOSTED_COMPUTERS`. Invalid pause settings fail closed. Network access follows
178
+ the host's admitted egress composition; a native container binding and the
179
+ on-box host adapter are separate configuration paths.
180
+
181
+ ## Image and build inputs
182
+
183
+ The [Dockerfile](Dockerfile) builds the Linux environment with pinned Node,
184
+ pnpm, Git, FUSE support and the bundled computerd. Its
185
+ [container manifest](container/package.json) owns the PTY and runtime
186
+ dependencies. [computer-init](container/computer-init.mjs) supervises computerd
187
+ and the terminal server. Initialization mounts before dropping privileges; the
188
+ image removes setuid/setgid helpers. Terminal shell assets live under
189
+ [container/terminal-assets](container/terminal-assets).
190
+
191
+ The [Forage dependency fixture](test/forage-dependencies/README.md) describes an
192
+ offline dependency layer and its exact source manifest. It is a reproducible
193
+ build fixture, not another runtime package or evidence that generated outputs
194
+ were saved in a hosted Computer. Do not copy its temporary output into source.
195
+
196
+ ## Proof and reference
197
+
198
+ [package.json](package.json) declares the private entrypoints `.` and `./files`,
199
+ `./backing`, `./host-config`, `./operations`.
200
+ [Runtime proof](test/runtime-proof.mjs) checks native workspace operations and
201
+ reconstruction. [Process proof](test/process-proof.mjs) checks retained process
202
+ identity against the real container.
203
+ [Terminal proof](test/terminal-container-proof.mjs),
204
+ [resource proof](test/container-resource-proof.mjs) and
205
+ [backend tests](test/computer-backends.test.mjs) cover their separate
206
+ boundaries. The declared nightly egress proof is separate from the ordinary
207
+ package test. No local proof alone qualifies a production or celld/on-box
208
+ deployment.
209
+
210
+ ### Post-command Workspace transport
211
+
212
+ The Computer's remote-backend adapter observes the backend's closed-connection
213
+ signal and classifies failed RPC calls and streams as transport failures
214
+ understood by `@cloudflare/computer`. It does not inspect exception text. The
215
+ library retains its existing one-reconnect bound and finishes the post-command
216
+ pull against the same runtime. The adapter adds no retry loop, never replays an
217
+ uncertain command, and leaves failed synchronization pending. Public consumers
218
+ use the unpatched dependency. `node --test test/workspace-sync.test.mjs`
219
+ exercises that installed Workspace runtime and SQLite filesystem, including
220
+ persisted file bytes, persistent failure, domain failure, runtime replacement
221
+ and uncertain dispatch.
222
+
223
+ ### Port previews
224
+
225
+ `@fungi.computer/computer/preview` owns bounded, signed preview tickets and
226
+ service-relative routing. The host supplies its selector decoder, hostname
227
+ policy and live admission callback. Admission runs at the exposed-port listing
228
+ and forwarding acceptance points; a ticket is never a grant. Hub retains its
229
+ local App projection and signed-ticket hostname configuration.
@@ -0,0 +1,119 @@
1
+ // The Computer image's only supervisor. tini is PID 1 and reaps orphans;
2
+ // this process owns computerd and the terminal server. It restarts neither:
3
+ // if either dies the container exits, the Containers monitor records the
4
+ // exit, and the Computer's container backend launches a fresh runtime.
5
+ import { spawn } from "node:child_process";
6
+ import process from "node:process";
7
+ import { setTimeout, clearTimeout } from "node:timers";
8
+ import { fileURLToPath, URL } from "node:url";
9
+ import { enterRuntimePhase } from "./runtime-privileges.mjs";
10
+ import { boundedRuntimeMs } from "./resource-bounds.mjs";
11
+
12
+ const computerd = "/opt/fungi/computerd.cjs";
13
+ const terminalPort = Number(process.env.COMPUTER_TERMINAL_PORT ?? "7681");
14
+ // computerd (cordyceps) drops its own privileges in-process, so it needs this
15
+ // module by path, not by name: it never names a sibling package. This
16
+ // supervisor is its only supplier, since it is the one process that knows
17
+ // where the container placed the privilege boundary.
18
+ const runtimePrivilegeModule = fileURLToPath(
19
+ new URL("./runtime-privileges.mjs", import.meta.url),
20
+ );
21
+
22
+ function log(message, fields) {
23
+ process.stderr.write(
24
+ `${JSON.stringify({ component: "computer-init", message, ...fields })}\n`,
25
+ );
26
+ }
27
+
28
+ let terminal;
29
+ const daemon = spawn(process.execPath, [computerd], {
30
+ stdio: ["ignore", "inherit", "inherit", "ipc"],
31
+ env: {
32
+ ...process.env,
33
+ COMPUTER_RUNTIME_PRIVILEGE_MODULE: runtimePrivilegeModule,
34
+ },
35
+ });
36
+ let stopping = false;
37
+ const maximumRuntimeMs = boundedRuntimeMs(process.env.COMPUTER_MAX_RUNTIME_MS);
38
+ setTimeout(() => {
39
+ log("runtime deadline reached", { maximumRuntimeMs });
40
+ stopping = true;
41
+ terminal?.kill("SIGTERM");
42
+ daemon.kill("SIGTERM");
43
+ setTimeout(() => process.exit(0), 5_000).unref();
44
+ }, maximumRuntimeMs).unref();
45
+
46
+ daemon.once("exit", (code, signal) => {
47
+ log("computerd exited", { code, signal, stopping });
48
+ terminal?.kill("SIGTERM");
49
+ // An unrequested exit is a failure even when computerd reports 0.
50
+ process.exit(stopping ? (code ?? 0) : 1);
51
+ });
52
+ daemon.once("error", (error) => {
53
+ log("computerd failed to start", { error: error.message });
54
+ terminal?.kill("SIGTERM");
55
+ process.exit(1);
56
+ });
57
+
58
+ try {
59
+ await new Promise((resolve, reject) => {
60
+ const timer = setTimeout(
61
+ () => reject(new Error("Computer mount phase timed out")),
62
+ 15_000,
63
+ );
64
+ const failed = () => {
65
+ clearTimeout(timer);
66
+ reject(new Error("Computer exited during its mount phase"));
67
+ };
68
+ daemon.once("exit", failed);
69
+ daemon.once("error", failed);
70
+ daemon.once("message", (message) => {
71
+ clearTimeout(timer);
72
+ daemon.off("exit", failed);
73
+ daemon.off("error", failed);
74
+ if (message?.type !== "runtime-unprivileged") {
75
+ reject(new Error("Unexpected Computer mount result"));
76
+ return;
77
+ }
78
+ daemon.disconnect();
79
+ resolve();
80
+ });
81
+ });
82
+ enterRuntimePhase();
83
+ // exec the terminal runtime with the permanent UID/GID and no-new-privs
84
+ // inherited. The mount daemon retains its live FUSE descriptor and VFS state.
85
+ terminal = spawn(process.execPath, ["/opt/fungi/terminal-runtime.mjs"], {
86
+ stdio: ["ignore", "inherit", "inherit", "ipc"],
87
+ env: process.env,
88
+ });
89
+ terminal.once("exit", () => {
90
+ daemon.kill("SIGTERM");
91
+ process.exit(stopping ? 0 : 1);
92
+ });
93
+ await new Promise((resolve, reject) => {
94
+ const timer = setTimeout(
95
+ () => reject(new Error("Terminal startup timed out")),
96
+ 15_000,
97
+ );
98
+ terminal.once("error", reject);
99
+ terminal.once("message", (message) => {
100
+ clearTimeout(timer);
101
+ if (message?.type !== "terminal-ready")
102
+ return reject(new Error("Unexpected Terminal startup result"));
103
+ terminal.disconnect();
104
+ resolve();
105
+ });
106
+ });
107
+ log("terminal listening", { port: terminalPort });
108
+ } catch (error) {
109
+ daemon.kill("SIGTERM");
110
+ throw new Error("Computer terminal failed to listen", { cause: error });
111
+ }
112
+
113
+ for (const signal of ["SIGTERM", "SIGINT"]) {
114
+ process.on(signal, () => {
115
+ stopping = true;
116
+ terminal?.kill("SIGTERM");
117
+ daemon.kill(signal);
118
+ });
119
+ }
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # A distinct real UID makes RLIMIT_NPROC independent even when local Docker
5
+ # containers share one kernel. The hard limit survives into shells and their
6
+ # children; an unprivileged Computer cannot raise it again.
7
+ uid_file=/opt/fungi/.computer-uid
8
+ if [[ -f "$uid_file" ]]; then
9
+ uid=$(<"$uid_file")
10
+ else
11
+ uid=$((100000 + $(od -An -N4 -tu4 /dev/urandom) % 2000000000))
12
+ printf '%s\n' "$uid" >"$uid_file"
13
+ fi
14
+ if ! getent passwd "$uid" >/dev/null; then
15
+ useradd --no-log-init --uid "$uid" --user-group --no-create-home --home-dir /workspace --shell /bin/bash fungi
16
+ fi
17
+ export COMPUTER_RUNTIME_UID="$uid"
18
+ pids_max=$(node --input-type=module -e 'import { computerResourceBounds } from "/opt/fungi/resource-bounds.mjs"; process.stdout.write(String(computerResourceBounds.pidsMax))')
19
+ mkdir -p /workspace
20
+ chown "$uid:$uid" /workspace
21
+ ulimit -Su "$pids_max"
22
+ ulimit -Hu "$pids_max"
23
+ export HOME=/workspace
24
+ exec node /opt/fungi/computer-init.mjs
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@fungi.computer/computer-container",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "In-container supervisor and terminal server for one Fungi Computer image",
6
+ "type": "module",
7
+ "dependencies": {
8
+ "node-pty": "1.1.0",
9
+ "ws": "8.21.3",
10
+ "fuse-native": "2.2.6",
11
+ "esbuild": "0.28.2"
12
+ },
13
+ "license": "MIT"
14
+ }