@fungi.computer/computer 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +85 -0
- package/LICENSE +21 -0
- package/README.md +228 -2
- package/container/computer-init.mjs +119 -0
- package/container/computer-start.sh +24 -0
- package/container/package.json +14 -0
- package/container/pnpm-lock.yaml +378 -0
- package/container/pnpm-workspace.yaml +5 -0
- package/container/resource-bounds.mjs +54 -0
- package/container/runtime-privileges.cc +37 -0
- package/container/runtime-privileges.mjs +11 -0
- package/container/terminal-assets/README.md +35 -0
- package/container/terminal-assets/bashrc +49 -0
- package/container/terminal-assets/git-prompt.sh +586 -0
- package/container/terminal-protocol.mjs +58 -0
- package/container/terminal-runtime.mjs +10 -0
- package/container/terminal-server.mjs +243 -0
- package/dist/apply-patch/LICENSE.codex +201 -0
- package/dist/apply-patch/build.mjs +98 -0
- package/dist/apply-patch/cache.mjs +168 -0
- package/dist/apply-patch/compile.mjs +199 -0
- package/dist/apply-patch/docker/darwin.Dockerfile +15 -0
- package/dist/apply-patch/docker/linux-arm64.Dockerfile +38 -0
- package/dist/apply-patch/docker/linux-x64.Dockerfile +23 -0
- package/dist/apply-patch/main.rs +320 -0
- package/dist/apply-patch/notices.mjs +44 -0
- package/dist/apply-patch/pin.mjs +8 -0
- package/dist/apply-patch/toolchain.mjs +39 -0
- package/dist/computer-backends.d.ts +94 -0
- package/dist/computer-backends.js +103 -0
- package/dist/computerd.cjs +18010 -0
- package/dist/container-policy.d.ts +7 -0
- package/dist/container-policy.js +24 -0
- package/dist/container-system-trust.d.ts +8 -0
- package/dist/container-system-trust.js +66 -0
- package/dist/egress-policy.d.ts +89 -0
- package/dist/egress-policy.js +240 -0
- package/dist/execution-result.d.ts +3 -0
- package/dist/execution-result.js +28 -0
- package/dist/files-patch.d.ts +20 -0
- package/dist/files-patch.js +108 -0
- package/dist/files.d.ts +75 -0
- package/dist/files.js +136 -0
- package/dist/fleet/admission.d.ts +14 -0
- package/dist/fleet/admission.js +10 -0
- package/dist/fleet/backing.d.ts +52 -0
- package/dist/fleet/backing.js +242 -0
- package/dist/fleet/computers.d.ts +62 -0
- package/dist/fleet/computers.js +35 -0
- package/dist/fleet/index.d.ts +11 -0
- package/dist/fleet/index.js +11 -0
- package/dist/fleet/inventory-wire.d.ts +29 -0
- package/dist/fleet/inventory-wire.js +15 -0
- package/dist/fleet/inventory.d.ts +24 -0
- package/dist/fleet/inventory.js +152 -0
- package/dist/fleet/lifecycle.d.ts +99 -0
- package/dist/fleet/lifecycle.js +418 -0
- package/dist/fleet/runtime-usage.d.ts +50 -0
- package/dist/fleet/runtime-usage.js +145 -0
- package/dist/fleet/slots.d.ts +30 -0
- package/dist/fleet/slots.js +46 -0
- package/dist/fleet/wire.d.ts +228 -0
- package/dist/fleet/wire.js +127 -0
- package/dist/host-config.d.ts +12 -0
- package/dist/host-config.js +10 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.js +1237 -0
- package/dist/light-shell.d.ts +44 -0
- package/dist/light-shell.js +376 -0
- package/dist/local-backend.d.ts +52 -0
- package/dist/local-backend.js +143 -0
- package/dist/metered-container-host.d.ts +26 -0
- package/dist/metered-container-host.js +48 -0
- package/dist/on-box-vm.d.ts +119 -0
- package/dist/on-box-vm.js +294 -0
- package/dist/operation-contract.d.ts +456 -0
- package/dist/operation-contract.js +450 -0
- package/dist/operation-record.d.ts +50 -0
- package/dist/operation-record.js +162 -0
- package/dist/preview.d.ts +55 -0
- package/dist/preview.js +151 -0
- package/dist/process-inventory.d.ts +80 -0
- package/dist/process-inventory.js +427 -0
- package/dist/pull-pending.d.ts +26 -0
- package/dist/pull-pending.js +35 -0
- package/dist/team-candidate-builder.mjs +28220 -0
- package/dist/transport-backend.d.ts +3 -0
- package/dist/transport-backend.js +99 -0
- package/dist/workspace-stream.d.ts +2 -0
- package/dist/workspace-stream.js +31 -0
- 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
|
-
#
|
|
1
|
+
# Computer runtime
|
|
2
2
|
|
|
3
|
-
This
|
|
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
|
+
}
|