@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.
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 +418 -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 +450 -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 +28220 -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
@@ -0,0 +1,55 @@
1
+ import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
2
+ import * as Schema from "effect/Schema";
3
+ /**
4
+ * Owns the persisted, bounded preview ticket and signed hostname. The caller
5
+ * must still check live Computer exposure and admission before issue and after
6
+ * consume; a ticket is not a grant.
7
+ */
8
+ export type ComputerPreviewPolicy<Selector> = {
9
+ selector: Schema.Codec<Selector, unknown>;
10
+ previewUrl: (signature: string, domain: string) => string;
11
+ accept: (selector: Selector) => void;
12
+ listPorts: (accept: () => void) => Promise<{
13
+ ports: number[];
14
+ }>;
15
+ forward: (port: number, request: Request, accept: () => void) => Promise<Response>;
16
+ portDenied: () => never;
17
+ dispatchFailure: (error: Error) => Response;
18
+ invalidClaims: () => void;
19
+ };
20
+ export declare class ComputerPortPreview<Selector> {
21
+ private readonly storage;
22
+ private readonly policy;
23
+ constructor(storage: Pick<WatchdogSqliteOwner, "sql"> & {
24
+ get(key: "computer_port_preview_signing_key"): Promise<string | undefined>;
25
+ put(key: "computer_port_preview_signing_key", value: string): Promise<void>;
26
+ }, policy: ComputerPreviewPolicy<Selector>);
27
+ /** Match only the signed host route and forward a service-relative URL. */
28
+ route(request: Request, dispatch: (signature: string, request: Request) => Promise<Response>): Promise<Response | null>;
29
+ issue(selector: Selector, port: number, previewOriginDomain: string): Promise<{
30
+ previewUrl: string;
31
+ expiresAt: number;
32
+ }>;
33
+ resolve(signature: string): Promise<{
34
+ selector: Selector;
35
+ port: number;
36
+ } | null>;
37
+ /** A signed ticket is not authority: both issue and dispatch recheck live admission and exposure. */
38
+ issueAfterAcceptance(rawSelector: Selector, port: number, origin: string): Promise<{
39
+ previewUrl: string;
40
+ expiresAt: number;
41
+ }>;
42
+ dispatch(rawSelector: Selector, port: number, request: Request): Promise<Response>;
43
+ dispatchSigned(signature: string, request: Request, forward: (selector: Selector, port: number, request: Request) => Promise<Response>): Promise<Response>;
44
+ private ensureTable;
45
+ private sign;
46
+ }
47
+ /** Mount an isolated preview host with the owning host's target and forwarding policy. */
48
+ export declare function computerPortPreviewRequest(request: Request, origin: string | null, host: {
49
+ suffix: (domain: string) => string;
50
+ target: (hostname: string, domain: string) => {
51
+ computerId: string;
52
+ signature: string;
53
+ } | null;
54
+ forward: (computerId: string, request: Request) => Promise<Response>;
55
+ }): Promise<Response | null>;
@@ -0,0 +1,151 @@
1
+ import { LONG, nanoid } from "@fungi.computer/cairn";
2
+ import * as Schema from "effect/Schema";
3
+ import * as Option from "effect/Option";
4
+ const storedClaimsSchema = Schema.Struct({
5
+ claims: Schema.String,
6
+ expires_at: Schema.Number,
7
+ });
8
+ const lifetimeMs = 5 * 60_000;
9
+ const encoder = new TextEncoder();
10
+ const previewPath = /^\/__port_preview\/([a-z2-7]{20})(\/.*)?$/u;
11
+ function base32(bytes) {
12
+ const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
13
+ let bits = 0;
14
+ let value = 0;
15
+ let result = "";
16
+ for (const byte of bytes) {
17
+ value = (value << 8) | byte;
18
+ bits += 8;
19
+ while (bits >= 5) {
20
+ result += alphabet.charAt((value >>> (bits - 5)) & 31);
21
+ bits -= 5;
22
+ }
23
+ }
24
+ if (bits > 0)
25
+ result += alphabet.charAt((value << (5 - bits)) & 31);
26
+ return result;
27
+ }
28
+ export class ComputerPortPreview {
29
+ storage;
30
+ policy;
31
+ constructor(storage, policy) {
32
+ this.storage = storage;
33
+ this.policy = policy;
34
+ }
35
+ /** Match only the signed host route and forward a service-relative URL. */
36
+ async route(request, dispatch) {
37
+ const url = new URL(request.url);
38
+ const preview = previewPath.exec(url.pathname);
39
+ if (preview === null)
40
+ return null;
41
+ const service = new URL("http://localhost");
42
+ service.pathname = preview[2] ?? "/";
43
+ service.search = url.search;
44
+ return await dispatch(preview[1] ?? "", new Request(service, request));
45
+ }
46
+ async issue(selector, port, previewOriginDomain) {
47
+ const expiresAt = Date.now() + lifetimeMs;
48
+ const claims = JSON.stringify({ selector, port, expiresAt });
49
+ const signature = await this.sign(claims);
50
+ this.ensureTable();
51
+ this.storage.sql.exec("DELETE FROM computer_port_preview_tickets WHERE expires_at<=?", Date.now());
52
+ this.storage.sql.exec("INSERT OR REPLACE INTO computer_port_preview_tickets(signature,claims,expires_at) VALUES(?,?,?)", signature, claims, expiresAt);
53
+ this.storage.sql.exec("DELETE FROM computer_port_preview_tickets WHERE signature NOT IN (SELECT signature FROM computer_port_preview_tickets ORDER BY expires_at DESC LIMIT 256)");
54
+ return {
55
+ previewUrl: this.policy.previewUrl(signature, previewOriginDomain),
56
+ expiresAt,
57
+ };
58
+ }
59
+ async resolve(signature) {
60
+ this.ensureTable();
61
+ const row = this.storage.sql
62
+ .exec("SELECT claims,expires_at FROM computer_port_preview_tickets WHERE signature=?", signature)
63
+ .toArray()[0];
64
+ if (row === undefined)
65
+ return null;
66
+ const stored = Schema.decodeUnknownSync(storedClaimsSchema)(row);
67
+ if (stored.expires_at <= Date.now())
68
+ return null;
69
+ if ((await this.sign(stored.claims)) !== signature)
70
+ return null;
71
+ const claimsSchema = Schema.Struct({
72
+ selector: this.policy.selector,
73
+ port: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween({ minimum: 1, maximum: 65_535 }))),
74
+ expiresAt: Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isGreaterThan(0))),
75
+ });
76
+ const decoded = Schema.decodeUnknownOption(Schema.fromJsonString(claimsSchema))(stored.claims);
77
+ if (Option.isNone(decoded)) {
78
+ this.policy.invalidClaims();
79
+ return null;
80
+ }
81
+ const claims = decoded.value;
82
+ return claims.expiresAt <= Date.now()
83
+ ? null
84
+ : { selector: claims.selector, port: claims.port };
85
+ }
86
+ /** A signed ticket is not authority: both issue and dispatch recheck live admission and exposure. */
87
+ async issueAfterAcceptance(rawSelector, port, origin) {
88
+ const selector = Schema.decodeUnknownSync(this.policy.selector)(rawSelector);
89
+ const ports = await this.policy.listPorts(() => {
90
+ this.policy.accept(selector);
91
+ });
92
+ if (!ports.ports.includes(port))
93
+ this.policy.portDenied();
94
+ return await this.issue(selector, port, origin);
95
+ }
96
+ async dispatch(rawSelector, port, request) {
97
+ const selector = Schema.decodeUnknownSync(this.policy.selector)(rawSelector);
98
+ try {
99
+ return await this.policy.forward(port, request, () => {
100
+ this.policy.accept(selector);
101
+ });
102
+ }
103
+ catch (error) {
104
+ if (!(error instanceof Error))
105
+ throw error;
106
+ return this.policy.dispatchFailure(error);
107
+ }
108
+ }
109
+ async dispatchSigned(signature, request, forward) {
110
+ const claims = await this.resolve(signature);
111
+ if (claims === null)
112
+ return new Response(null, { status: 403 });
113
+ return await forward(claims.selector, claims.port, request);
114
+ }
115
+ ensureTable() {
116
+ this.storage.sql.exec("CREATE TABLE IF NOT EXISTS computer_port_preview_tickets (signature TEXT PRIMARY KEY, claims TEXT NOT NULL, expires_at INTEGER NOT NULL) STRICT");
117
+ }
118
+ async sign(claims) {
119
+ const storageKey = "computer_port_preview_signing_key";
120
+ let secret = Schema.decodeUnknownSync(Schema.UndefinedOr(Schema.String))(await this.storage.get(storageKey));
121
+ if (secret === undefined) {
122
+ secret = nanoid(LONG) + nanoid(LONG);
123
+ await this.storage.put(storageKey, secret);
124
+ }
125
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
126
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(claims));
127
+ return base32(new Uint8Array(signature).slice(0, 12));
128
+ }
129
+ }
130
+ /** Mount an isolated preview host with the owning host's target and forwarding policy. */
131
+ export async function computerPortPreviewRequest(request, origin, host) {
132
+ if (origin === null)
133
+ return null;
134
+ const incoming = new URL(request.url);
135
+ if (incoming.protocol !== "https:" ||
136
+ !incoming.hostname.endsWith(host.suffix(origin)))
137
+ return null;
138
+ const target = host.target(incoming.hostname, origin);
139
+ if (target === null)
140
+ return new Response(null, { status: 404 });
141
+ const { computerId, signature } = target;
142
+ const forwarded = new Request(request.url, request);
143
+ try {
144
+ const destination = new URL(forwarded.url);
145
+ destination.pathname = `/__port_preview/${signature}${destination.pathname}`;
146
+ return await host.forward(computerId, new Request(destination, forwarded));
147
+ }
148
+ catch {
149
+ return new Response(null, { status: 503 });
150
+ }
151
+ }
@@ -0,0 +1,80 @@
1
+ import type { WorkspaceRuntimeEvent, WorkspaceRuntimeExecHandle } from "@fungi.computer/cloudflare-computer";
2
+ import { z } from "zod";
3
+ import { processStartInputSchema, type ComputerProcess, type ComputerProcessId, type ComputerProcessSync } from "./operation-contract.js";
4
+ /** The durable SQLite surface owned by processes and their port exposures. */
5
+ export type ComputerProcessStorage = Pick<DurableObjectStorage, "sql" | "transactionSync">;
6
+ /** Computer-owned replay identities; computerd owns execution and exit observations. */
7
+ export declare class ComputerProcessInventory {
8
+ #private;
9
+ constructor(storage: ComputerProcessStorage, now: () => number);
10
+ /** A replay does not boot a backend or redispatch an exited command. */
11
+ replay(input: z.output<typeof processStartInputSchema>, inputHash: string, accept: () => void): ComputerProcess | undefined;
12
+ /** Reserve the process and only a newly created exposure in one transaction. */
13
+ admit(input: z.output<typeof processStartInputSchema>, inputHash: string, accept: () => void): Readonly<{
14
+ process: ComputerProcess;
15
+ created: boolean;
16
+ }>;
17
+ exited(id: ComputerProcessId, exitCode: number | null, sync?: ComputerProcessSync): ComputerProcess;
18
+ /** Retain a failed dispatch receipt: retrying that key cannot create new work. */
19
+ release(id: ComputerProcessId): void;
20
+ /**
21
+ * Upgrade an already-exited row's sync verdict once a background drain
22
+ * (one that gave up synchronously at its deadline) resolves it. A
23
+ * no-op if the row was evicted or never exited: pending rows must be
24
+ * updatable here, not only running ones.
25
+ */
26
+ resync(id: ComputerProcessId, sync: ComputerProcessSync): void;
27
+ get(id: ComputerProcessId): ComputerProcess | undefined;
28
+ list(): ComputerProcess[];
29
+ }
30
+ /** What one liveness observation of a computerd execution proved. */
31
+ export type ProcessObservation = Readonly<{
32
+ state: "running";
33
+ }> | Readonly<{
34
+ state: "exited";
35
+ exitCode: number | null;
36
+ sync: ComputerProcessSync;
37
+ /**
38
+ * Present only when `sync` is "pending" because a bounded drain
39
+ * gave up waiting: resolves once a background continuation reaches
40
+ * the real EOF (or gives up on an error), confirming the pull
41
+ * landed. The caller owns keeping the host alive for it (a
42
+ * Durable Object's `waitUntil`) and applying the upgrade
43
+ * (`ComputerProcessInventory.resync`) once it resolves.
44
+ */
45
+ settleLater?: Promise<ComputerProcessSync>;
46
+ }> | Readonly<{
47
+ state: "uncertain";
48
+ }>;
49
+ /** The part of the Workspace runtime a liveness probe uses, on one backend. */
50
+ export type ProcessRuntime = Readonly<{
51
+ getExec: (id: string, options: {
52
+ resume: "tail" | "full";
53
+ }) => Promise<ReadableStream<WorkspaceRuntimeEvent<"utf8">> & Disposable>;
54
+ }>;
55
+ /** How long a tail read may stay silent before the process counts as live. */
56
+ export declare const liveProbeMs = 750;
57
+ /**
58
+ * How long a drain may keep waiting for EOF once an exit has already been
59
+ * observed, before giving up: a backgrounded child (for example
60
+ * `node server.js &`) can hold the transport open long past its own
61
+ * parent's exit, and nothing here may hang on that forever.
62
+ */
63
+ export declare const postExitDrainDeadlineMs = 2000;
64
+ /**
65
+ * Observe one execution without consuming its output: a tail attach emits
66
+ * nothing for a live process, closes at once for an exited one, and fails
67
+ * for one computerd no longer holds.
68
+ */
69
+ export declare function observeProcess(runtime: ProcessRuntime, id: string): Promise<ProcessObservation>;
70
+ /**
71
+ * Drain `run` to its exit, bounded past that exit the same way
72
+ * `observeProcess` is, and determine whether its files reached the
73
+ * Workspace. `undefined` if the stream broke or was replaced before any
74
+ * exit was observed -- there is nothing to record.
75
+ */
76
+ export declare function drainProcessExit(run: WorkspaceRuntimeExecHandle<"utf8">): Promise<Readonly<{
77
+ exitCode: number;
78
+ sync: ComputerProcessSync;
79
+ settleLater?: Promise<ComputerProcessSync>;
80
+ }> | undefined>;