@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.
- 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 +423 -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 +452 -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 +28170 -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/dist/files.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { replaceOutputSchema, } from "./operation-contract.js";
|
|
2
|
+
const utf8 = new TextEncoder();
|
|
3
|
+
function entryType(entry) {
|
|
4
|
+
if (entry.isSymbolicLink)
|
|
5
|
+
return "symlink";
|
|
6
|
+
if (entry.isFile)
|
|
7
|
+
return "file";
|
|
8
|
+
if (entry.isDirectory)
|
|
9
|
+
return "directory";
|
|
10
|
+
return "symlink";
|
|
11
|
+
}
|
|
12
|
+
function childPath(parent, name) {
|
|
13
|
+
return parent === "/" ? `/${name}` : `${parent}/${name}`;
|
|
14
|
+
}
|
|
15
|
+
function decodeUtf8(bytes, path) {
|
|
16
|
+
return decodeUtf8Page(bytes, path, false).content;
|
|
17
|
+
}
|
|
18
|
+
/** Decode a bounded byte page without consuming an incomplete final UTF-8 character. */
|
|
19
|
+
export function decodeUtf8Page(bytes, path, moreBytesRemain) {
|
|
20
|
+
try {
|
|
21
|
+
const content = new TextDecoder("utf-8", {
|
|
22
|
+
fatal: true,
|
|
23
|
+
ignoreBOM: true,
|
|
24
|
+
}).decode(bytes, { stream: moreBytesRemain });
|
|
25
|
+
const byteLength = utf8.encode(content).byteLength;
|
|
26
|
+
if (byteLength === 0 && bytes.byteLength > 0) {
|
|
27
|
+
throw new Error(`Files read cannot advance a UTF-8 page: ${path}`);
|
|
28
|
+
}
|
|
29
|
+
return { content, byteLength };
|
|
30
|
+
}
|
|
31
|
+
catch (cause) {
|
|
32
|
+
throw new Error(`Files path is not valid UTF-8: ${path}`, { cause });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function readBytes(workspace, path, byteOffset, byteLength) {
|
|
36
|
+
const options = {
|
|
37
|
+
byteOffset,
|
|
38
|
+
byteLength,
|
|
39
|
+
};
|
|
40
|
+
const source = await workspace.fs.readFile(path, options);
|
|
41
|
+
return new Uint8Array(await new Response(source).arrayBuffer());
|
|
42
|
+
}
|
|
43
|
+
/** Read raw bounded bytes without Workspace.fs's BOM-stripping UTF-8 path. */
|
|
44
|
+
export function readBoundedWorkspaceText(provider, path, maximumBytes) {
|
|
45
|
+
const stat = provider.statSync(path);
|
|
46
|
+
if (!stat.isFile())
|
|
47
|
+
throw new Error(`Files path is not a file: ${path}`);
|
|
48
|
+
if (stat.size > maximumBytes) {
|
|
49
|
+
throw new Error(`Files path exceeds the ${String(maximumBytes)}-byte bound: ${path}`);
|
|
50
|
+
}
|
|
51
|
+
return decodeUtf8(new Uint8Array(provider.readRangeSync(path, 0, stat.size)), path);
|
|
52
|
+
}
|
|
53
|
+
/** Adapt native Workspace.fs at the one real file boundary. */
|
|
54
|
+
export function createWorkspaceFilesPort(workspace, owner) {
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
async read(input) {
|
|
57
|
+
const stat = await workspace.fs.stat(input.path);
|
|
58
|
+
if (!stat.isFile)
|
|
59
|
+
throw new Error(`Files path is not a file: ${input.path}`);
|
|
60
|
+
if (input.offset > stat.size) {
|
|
61
|
+
throw new Error(`Files read offset exceeds file size: ${input.path}`);
|
|
62
|
+
}
|
|
63
|
+
if (input.offset === stat.size) {
|
|
64
|
+
return {
|
|
65
|
+
path: input.path,
|
|
66
|
+
content: "",
|
|
67
|
+
offset: input.offset,
|
|
68
|
+
nextOffset: null,
|
|
69
|
+
truncated: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const pageBytes = await readBytes(workspace, input.path, input.offset, Math.min(input.limit, stat.size - input.offset));
|
|
73
|
+
if (pageBytes.byteLength === 0) {
|
|
74
|
+
throw new Error(`Files path changed during read: ${input.path}`);
|
|
75
|
+
}
|
|
76
|
+
const page = decodeUtf8Page(pageBytes, input.path, input.offset + pageBytes.byteLength < stat.size);
|
|
77
|
+
const nextOffset = input.offset + page.byteLength;
|
|
78
|
+
return {
|
|
79
|
+
path: input.path,
|
|
80
|
+
content: page.content,
|
|
81
|
+
offset: input.offset,
|
|
82
|
+
nextOffset: nextOffset < stat.size ? nextOffset : null,
|
|
83
|
+
truncated: nextOffset < stat.size,
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
async list(input) {
|
|
87
|
+
const rows = await workspace.fs.readdir(input.path, {
|
|
88
|
+
limit: input.limit + 1,
|
|
89
|
+
});
|
|
90
|
+
const entries = rows.slice(0, input.limit).map((entry) => {
|
|
91
|
+
const path = childPath(input.path, entry.name);
|
|
92
|
+
return {
|
|
93
|
+
path,
|
|
94
|
+
name: entry.name,
|
|
95
|
+
type: entryType(entry),
|
|
96
|
+
size: entry.size,
|
|
97
|
+
modifiedAt: entry.mtime,
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
return {
|
|
101
|
+
path: input.path,
|
|
102
|
+
entries,
|
|
103
|
+
truncated: rows.length > input.limit,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
async glob(input) {
|
|
107
|
+
const rows = await workspace.fs.find(input.path, input.pattern, {
|
|
108
|
+
limit: input.limit + 1,
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
path: input.path,
|
|
112
|
+
matches: rows.slice(0, input.limit).map((entry) => entry.path),
|
|
113
|
+
truncated: rows.length > input.limit,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
async grep(input) {
|
|
117
|
+
const rows = await workspace.fs.grep(input.pattern, input.path, {
|
|
118
|
+
regex: input.regex,
|
|
119
|
+
ignoreCase: input.ignoreCase,
|
|
120
|
+
limit: input.limit + 1,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
path: input.path,
|
|
124
|
+
matches: rows.slice(0, input.limit).map((row) => ({
|
|
125
|
+
path: row.path,
|
|
126
|
+
line: row.line,
|
|
127
|
+
text: row.text,
|
|
128
|
+
})),
|
|
129
|
+
truncated: rows.length > input.limit,
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
async replace(input) {
|
|
133
|
+
return replaceOutputSchema.parse(await owner.replaceFileIfUnchanged(input));
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComputerAction, ComputerGrant } from "./wire.js";
|
|
2
|
+
/** The fleet's decision contains no host policy or host lookup. */
|
|
3
|
+
export type ComputerAdmission = Readonly<{
|
|
4
|
+
kind: "admitted";
|
|
5
|
+
}> | Readonly<{
|
|
6
|
+
kind: "Refused";
|
|
7
|
+
reason: "computer" | "action" | "expired";
|
|
8
|
+
}>;
|
|
9
|
+
/** Check authority supplied by the host at its door, using the fleet's clock. */
|
|
10
|
+
export declare function checkComputerGrant(grant: ComputerGrant, request: Readonly<{
|
|
11
|
+
computerId: string;
|
|
12
|
+
action: ComputerAction;
|
|
13
|
+
now: number;
|
|
14
|
+
}>): ComputerAdmission;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Check authority supplied by the host at its door, using the fleet's clock. */
|
|
2
|
+
export function checkComputerGrant(grant, request) {
|
|
3
|
+
if (grant.computerId !== request.computerId)
|
|
4
|
+
return { kind: "Refused", reason: "computer" };
|
|
5
|
+
if (!grant.actions.has(request.action))
|
|
6
|
+
return { kind: "Refused", reason: "action" };
|
|
7
|
+
if (request.now >= grant.expiresAt)
|
|
8
|
+
return { kind: "Refused", reason: "expired" };
|
|
9
|
+
return { kind: "admitted" };
|
|
10
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
2
|
+
import { type AttachedBackend, type HostedBacking } from "../computer-backends.js";
|
|
3
|
+
import { type ComputerBackingChange, type ComputerBackendsProjection, type LocalBackendAttachInput, type LocalBackendDetachInput } from "./wire.js";
|
|
4
|
+
/** Own backing records without consulting deployment configuration or a host. */
|
|
5
|
+
export declare function createFleetBacking(owner: WatchdogSqliteOwner): {
|
|
6
|
+
ensure: () => void;
|
|
7
|
+
resolve: (computerId: string) => AttachedBackend[];
|
|
8
|
+
defaultBackings: () => ({
|
|
9
|
+
kind: "platform-container";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "on-box-vm";
|
|
12
|
+
hostId: string;
|
|
13
|
+
})[] | null;
|
|
14
|
+
hasCreationRecord: (computerId: string) => boolean;
|
|
15
|
+
computerCreated: (computerId: string, raw: readonly HostedBacking[]) => ComputerBackendsProjection;
|
|
16
|
+
change: (actor: string, raw: ComputerBackingChange, config: Readonly<{
|
|
17
|
+
offered: ReadonlySet<HostedBacking["kind"]>;
|
|
18
|
+
computerIds: readonly string[];
|
|
19
|
+
}>) => {
|
|
20
|
+
revision: number;
|
|
21
|
+
projections: Readonly<{
|
|
22
|
+
computerId: string;
|
|
23
|
+
revision: number;
|
|
24
|
+
backends: readonly AttachedBackend[];
|
|
25
|
+
}>[];
|
|
26
|
+
};
|
|
27
|
+
attachLocal: (actor: string, raw: LocalBackendAttachInput) => {
|
|
28
|
+
backendId: string;
|
|
29
|
+
projections: Readonly<{
|
|
30
|
+
computerId: string;
|
|
31
|
+
revision: number;
|
|
32
|
+
backends: readonly AttachedBackend[];
|
|
33
|
+
}>[];
|
|
34
|
+
};
|
|
35
|
+
detachLocal: (actor: string, raw: LocalBackendDetachInput) => {
|
|
36
|
+
detached: boolean;
|
|
37
|
+
projections: Readonly<{
|
|
38
|
+
computerId: string;
|
|
39
|
+
revision: number;
|
|
40
|
+
backends: readonly AttachedBackend[];
|
|
41
|
+
}>[];
|
|
42
|
+
};
|
|
43
|
+
localOwners: (computerId: string) => ReadonlyMap<string, string>;
|
|
44
|
+
detachActorLocal: (actor: string) => {
|
|
45
|
+
detached: number;
|
|
46
|
+
projections: Readonly<{
|
|
47
|
+
computerId: string;
|
|
48
|
+
revision: number;
|
|
49
|
+
backends: readonly AttachedBackend[];
|
|
50
|
+
}>[];
|
|
51
|
+
};
|
|
52
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { attachedBackendId, localBackendSchema, } from "../computer-backends.js";
|
|
3
|
+
import { ComputerBackingError, computerBackingChangeSchema, hostedBackingsSchema, localBackendAttachSchema, localBackendDetachSchema, localBackendDetachRequestSchema, } from "./wire.js";
|
|
4
|
+
const maximumLocalBackends = 8;
|
|
5
|
+
const localRowSchema = z.strictObject({
|
|
6
|
+
computer_id: z.string(),
|
|
7
|
+
device_id: z.string(),
|
|
8
|
+
actor_id: z.string(),
|
|
9
|
+
public_key: z.string(),
|
|
10
|
+
host_label: z.string(),
|
|
11
|
+
folder_label: z.string(),
|
|
12
|
+
request_id: z.string(),
|
|
13
|
+
});
|
|
14
|
+
function mintDeviceId() {
|
|
15
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz234567";
|
|
16
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
17
|
+
return Array.from(bytes, (byte) => alphabet.charAt(byte & 31)).join("");
|
|
18
|
+
}
|
|
19
|
+
/** Own backing records without consulting deployment configuration or a host. */
|
|
20
|
+
export function createFleetBacking(owner) {
|
|
21
|
+
const { sql } = owner;
|
|
22
|
+
const ensure = () => {
|
|
23
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS computer_fleet_backends (
|
|
24
|
+
scope TEXT PRIMARY KEY,
|
|
25
|
+
backings TEXT NOT NULL,
|
|
26
|
+
changed_at INTEGER NOT NULL,
|
|
27
|
+
changed_by TEXT NOT NULL
|
|
28
|
+
) STRICT`);
|
|
29
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS computer_fleet_backing_revision (
|
|
30
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
31
|
+
revision INTEGER NOT NULL CHECK (revision >= 0)
|
|
32
|
+
) STRICT`);
|
|
33
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS computer_fleet_local_backends (
|
|
34
|
+
computer_id TEXT NOT NULL,
|
|
35
|
+
device_id TEXT NOT NULL,
|
|
36
|
+
actor_id TEXT NOT NULL,
|
|
37
|
+
public_key TEXT NOT NULL,
|
|
38
|
+
host_label TEXT NOT NULL,
|
|
39
|
+
folder_label TEXT NOT NULL,
|
|
40
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
41
|
+
attached_at INTEGER NOT NULL,
|
|
42
|
+
PRIMARY KEY (computer_id, device_id)
|
|
43
|
+
) STRICT`);
|
|
44
|
+
sql.exec(`CREATE TABLE IF NOT EXISTS computer_fleet_cancelled_attaches (
|
|
45
|
+
actor_id TEXT NOT NULL,
|
|
46
|
+
request_id TEXT NOT NULL,
|
|
47
|
+
PRIMARY KEY (actor_id, request_id)
|
|
48
|
+
) STRICT`);
|
|
49
|
+
};
|
|
50
|
+
const revision = () => z
|
|
51
|
+
.number()
|
|
52
|
+
.int()
|
|
53
|
+
.nonnegative()
|
|
54
|
+
.parse(sql
|
|
55
|
+
.exec("SELECT revision FROM computer_fleet_backing_revision WHERE singleton=1")
|
|
56
|
+
.toArray()[0]?.revision ?? 0);
|
|
57
|
+
const advance = () => {
|
|
58
|
+
sql.exec("INSERT INTO computer_fleet_backing_revision VALUES (1,1) ON CONFLICT(singleton) DO UPDATE SET revision = revision + 1");
|
|
59
|
+
};
|
|
60
|
+
const stored = (scope) => {
|
|
61
|
+
const row = sql
|
|
62
|
+
.exec("SELECT backings FROM computer_fleet_backends WHERE scope = ?", scope)
|
|
63
|
+
.toArray()[0];
|
|
64
|
+
return row === undefined
|
|
65
|
+
? null
|
|
66
|
+
: hostedBackingsSchema.parse(JSON.parse(z.string().parse(row.backings)));
|
|
67
|
+
};
|
|
68
|
+
const selectLocal = "SELECT computer_id, device_id, actor_id, public_key, host_label, folder_label, request_id FROM computer_fleet_local_backends";
|
|
69
|
+
const localRows = (computerId) => sql
|
|
70
|
+
.exec(`${selectLocal} WHERE computer_id = ? ORDER BY attached_at, device_id`, computerId)
|
|
71
|
+
.toArray()
|
|
72
|
+
.map((row) => localRowSchema.parse(row));
|
|
73
|
+
const localBackend = (row) => localBackendSchema.parse({
|
|
74
|
+
kind: "local",
|
|
75
|
+
deviceId: row.device_id,
|
|
76
|
+
publicKey: row.public_key,
|
|
77
|
+
hostLabel: row.host_label,
|
|
78
|
+
folderLabel: row.folder_label,
|
|
79
|
+
});
|
|
80
|
+
const resolve = (computerId) => {
|
|
81
|
+
const created = stored(`created:${computerId}`);
|
|
82
|
+
if (created === null)
|
|
83
|
+
throw new ComputerBackingError("unknown_computer");
|
|
84
|
+
return [
|
|
85
|
+
...localRows(computerId).map(localBackend),
|
|
86
|
+
...(stored(`computer:${computerId}`) ?? stored("fleet") ?? created),
|
|
87
|
+
];
|
|
88
|
+
};
|
|
89
|
+
const projection = (computerId) => ({
|
|
90
|
+
computerId,
|
|
91
|
+
revision: revision(),
|
|
92
|
+
backends: resolve(computerId),
|
|
93
|
+
});
|
|
94
|
+
/** Creation takes a plain decision; retries cannot replace the first decision. */
|
|
95
|
+
const computerCreated = (computerId, raw) => {
|
|
96
|
+
const backings = hostedBackingsSchema.parse(raw);
|
|
97
|
+
return owner.transactionSync(() => {
|
|
98
|
+
sql.exec("INSERT INTO computer_fleet_backends VALUES (?,?,?,?) ON CONFLICT(scope) DO NOTHING", `created:${computerId}`, JSON.stringify(backings), Date.now(), "computer-create");
|
|
99
|
+
return projection(computerId);
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
/** The host passes the offered kinds; the fleet does not choose deployment policy. */
|
|
103
|
+
const change = (actor, raw, config) => {
|
|
104
|
+
const input = computerBackingChangeSchema.parse(raw);
|
|
105
|
+
if (input.backings?.some((backing) => !config.offered.has(backing.kind)))
|
|
106
|
+
throw new ComputerBackingError("not_enabled");
|
|
107
|
+
if (input.computerId === undefined && input.backings === null)
|
|
108
|
+
throw new ComputerBackingError("invalid");
|
|
109
|
+
const computerIds = input.computerId === undefined ? config.computerIds : [input.computerId];
|
|
110
|
+
return owner.transactionSync(() => {
|
|
111
|
+
for (const computerId of computerIds)
|
|
112
|
+
resolve(computerId);
|
|
113
|
+
const scope = input.computerId === undefined
|
|
114
|
+
? "fleet"
|
|
115
|
+
: `computer:${input.computerId}`;
|
|
116
|
+
if (input.backings === null)
|
|
117
|
+
sql.exec("DELETE FROM computer_fleet_backends WHERE scope = ?", scope);
|
|
118
|
+
else
|
|
119
|
+
sql.exec("INSERT INTO computer_fleet_backends VALUES (?,?,?,?) ON CONFLICT(scope) DO UPDATE SET backings = excluded.backings, changed_at = excluded.changed_at, changed_by = excluded.changed_by", scope, JSON.stringify(input.backings), Date.now(), actor);
|
|
120
|
+
advance();
|
|
121
|
+
return { revision: revision(), projections: computerIds.map(projection) };
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* An actor attaches a machine. Replaying the same request returns the
|
|
126
|
+
* same backend; the device id is minted here, the key is the machine's.
|
|
127
|
+
*/
|
|
128
|
+
const attachLocal = (actor, raw) => {
|
|
129
|
+
const input = localBackendAttachSchema.parse(raw);
|
|
130
|
+
if (stored(`created:${input.computerId}`) === null)
|
|
131
|
+
throw new ComputerBackingError("unknown_computer");
|
|
132
|
+
return owner.transactionSync(() => {
|
|
133
|
+
if (sql
|
|
134
|
+
.exec("SELECT 1 FROM computer_fleet_cancelled_attaches WHERE actor_id = ? AND request_id = ?", actor, input.requestId)
|
|
135
|
+
.toArray().length > 0)
|
|
136
|
+
throw new ComputerBackingError("request_conflict");
|
|
137
|
+
const prior = sql
|
|
138
|
+
.exec(`${selectLocal} WHERE request_id = ?`, input.requestId)
|
|
139
|
+
.toArray()[0];
|
|
140
|
+
if (prior !== undefined) {
|
|
141
|
+
const row = localRowSchema.parse(prior);
|
|
142
|
+
if (row.computer_id !== input.computerId ||
|
|
143
|
+
row.actor_id !== actor ||
|
|
144
|
+
row.public_key !== input.publicKey ||
|
|
145
|
+
row.host_label !== input.hostLabel ||
|
|
146
|
+
row.folder_label !== input.folderLabel)
|
|
147
|
+
throw new ComputerBackingError("request_conflict");
|
|
148
|
+
return {
|
|
149
|
+
backendId: attachedBackendId(localBackend(row)),
|
|
150
|
+
projections: [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
if (localRows(input.computerId).length >= maximumLocalBackends)
|
|
154
|
+
throw new ComputerBackingError("local_limit");
|
|
155
|
+
const deviceId = mintDeviceId();
|
|
156
|
+
sql.exec("INSERT INTO computer_fleet_local_backends (computer_id, device_id, actor_id, public_key, host_label, folder_label, request_id, attached_at) VALUES (?,?,?,?,?,?,?,?)", input.computerId, deviceId, actor, input.publicKey, input.hostLabel, input.folderLabel, input.requestId, Date.now());
|
|
157
|
+
advance();
|
|
158
|
+
return {
|
|
159
|
+
backendId: `local-${deviceId}`,
|
|
160
|
+
projections: [projection(input.computerId)],
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
/** Which actor attached each local backend on a Computer. */
|
|
165
|
+
const localOwners = (computerId) => new Map(localRows(computerId).map((row) => [
|
|
166
|
+
`local-${row.device_id}`,
|
|
167
|
+
row.actor_id,
|
|
168
|
+
]));
|
|
169
|
+
/**
|
|
170
|
+
* Detach one local backend. Only the actor who attached it may; a detach
|
|
171
|
+
* of one that is already gone is a no-op so an uncertain retry settles.
|
|
172
|
+
*/
|
|
173
|
+
const detachLocal = (actor, raw) => {
|
|
174
|
+
const input = z
|
|
175
|
+
.union([localBackendDetachSchema, localBackendDetachRequestSchema])
|
|
176
|
+
.parse(raw);
|
|
177
|
+
if (stored(`created:${input.computerId}`) === null)
|
|
178
|
+
throw new ComputerBackingError("unknown_computer");
|
|
179
|
+
return owner.transactionSync(() => {
|
|
180
|
+
const row = "requestId" in input
|
|
181
|
+
? sql
|
|
182
|
+
.exec(`${selectLocal} WHERE computer_id = ? AND request_id = ?`, input.computerId, input.requestId)
|
|
183
|
+
.toArray()[0]
|
|
184
|
+
: sql
|
|
185
|
+
.exec(`${selectLocal} WHERE computer_id = ? AND device_id = ?`, input.computerId, input.backendId.slice("local-".length))
|
|
186
|
+
.toArray()[0];
|
|
187
|
+
const attached = row === undefined ? undefined : localRowSchema.parse(row);
|
|
188
|
+
if (attached && attached.actor_id !== actor)
|
|
189
|
+
throw new ComputerBackingError("not_owner");
|
|
190
|
+
const requestId = "requestId" in input ? input.requestId : undefined;
|
|
191
|
+
if (requestId !== undefined) {
|
|
192
|
+
const cancelled = sql
|
|
193
|
+
.exec("SELECT 1 FROM computer_fleet_cancelled_attaches WHERE actor_id = ? AND request_id = ?", actor, requestId)
|
|
194
|
+
.toArray().length > 0;
|
|
195
|
+
if (!cancelled) {
|
|
196
|
+
// Retain cancellation for the fleet lifetime: attach request IDs have no expiry.
|
|
197
|
+
// The indexed per-actor ceiling bounds durable state without deleting protection.
|
|
198
|
+
const count = z
|
|
199
|
+
.number()
|
|
200
|
+
.parse(sql
|
|
201
|
+
.exec("SELECT count(*) AS count FROM computer_fleet_cancelled_attaches WHERE actor_id = ?", actor)
|
|
202
|
+
.toArray()[0]?.count);
|
|
203
|
+
if (count >= 4096)
|
|
204
|
+
throw new ComputerBackingError("local_limit");
|
|
205
|
+
sql.exec("INSERT INTO computer_fleet_cancelled_attaches VALUES (?,?)", actor, requestId);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (attached === undefined)
|
|
209
|
+
return { detached: false, projections: [] };
|
|
210
|
+
sql.exec("DELETE FROM computer_fleet_local_backends WHERE computer_id = ? AND device_id = ?", input.computerId, attached.device_id);
|
|
211
|
+
advance();
|
|
212
|
+
return { detached: true, projections: [projection(input.computerId)] };
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
/** The host withdraws an actor's devices; membership policy stays with that host. */
|
|
216
|
+
const detachActorLocal = (actor) => owner.transactionSync(() => {
|
|
217
|
+
const computerIds = sql
|
|
218
|
+
.exec("SELECT computer_id FROM computer_fleet_local_backends WHERE actor_id = ?", actor)
|
|
219
|
+
.toArray()
|
|
220
|
+
.map((row) => z.string().parse(row.computer_id));
|
|
221
|
+
if (computerIds.length === 0)
|
|
222
|
+
return { detached: 0, projections: [] };
|
|
223
|
+
sql.exec("DELETE FROM computer_fleet_local_backends WHERE actor_id = ?", actor);
|
|
224
|
+
advance();
|
|
225
|
+
return {
|
|
226
|
+
detached: computerIds.length,
|
|
227
|
+
projections: [...new Set(computerIds)].map(projection),
|
|
228
|
+
};
|
|
229
|
+
});
|
|
230
|
+
return {
|
|
231
|
+
ensure,
|
|
232
|
+
resolve,
|
|
233
|
+
defaultBackings: () => stored("fleet"),
|
|
234
|
+
hasCreationRecord: (computerId) => stored(`created:${computerId}`) !== null,
|
|
235
|
+
computerCreated,
|
|
236
|
+
change,
|
|
237
|
+
attachLocal,
|
|
238
|
+
detachLocal,
|
|
239
|
+
localOwners,
|
|
240
|
+
detachActorLocal,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
2
|
+
import type { HostedBacking } from "../computer-backends.js";
|
|
3
|
+
import type { FleetActor, ComputerCreateRequest } from "./inventory-wire.js";
|
|
4
|
+
/** Durable collection owner. Host decisions enter as data; projections leave in the result. */
|
|
5
|
+
export declare function createFleetComputers(owner: WatchdogSqliteOwner, capacity: Readonly<{
|
|
6
|
+
limit: number;
|
|
7
|
+
retryAfterSeconds: number;
|
|
8
|
+
}>): {
|
|
9
|
+
ensure: () => void;
|
|
10
|
+
read: (computerId: string) => import("./inventory-wire.js").FleetComputer | null;
|
|
11
|
+
list: (input: import("./inventory-wire.js").ComputerPage) => {
|
|
12
|
+
readonly computers: Readonly<{
|
|
13
|
+
computerId: string;
|
|
14
|
+
name: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
}>[];
|
|
18
|
+
readonly nextOffset: number | null;
|
|
19
|
+
};
|
|
20
|
+
count: () => number;
|
|
21
|
+
ids: () => string[];
|
|
22
|
+
/** A replay preserves the original decision and stages nothing. */
|
|
23
|
+
create: (actor: FleetActor, request: ComputerCreateRequest, backings: readonly HostedBacking[]) => {
|
|
24
|
+
computer: Readonly<{
|
|
25
|
+
computerId: string;
|
|
26
|
+
name: string;
|
|
27
|
+
createdAt: number;
|
|
28
|
+
updatedAt: number;
|
|
29
|
+
}>;
|
|
30
|
+
projections: Readonly<{
|
|
31
|
+
computerId: string;
|
|
32
|
+
revision: number;
|
|
33
|
+
backends: readonly import("../computer-backends.js").AttachedBackend[];
|
|
34
|
+
}>[];
|
|
35
|
+
};
|
|
36
|
+
readBacking: (computerId: string) => ({
|
|
37
|
+
kind: "platform-container";
|
|
38
|
+
} | {
|
|
39
|
+
kind: "on-box-vm";
|
|
40
|
+
hostId: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "local";
|
|
43
|
+
deviceId: string;
|
|
44
|
+
publicKey: string;
|
|
45
|
+
hostLabel: string;
|
|
46
|
+
folderLabel: string;
|
|
47
|
+
})[] | null;
|
|
48
|
+
reserveSlot: (computerId: string) => Readonly<{
|
|
49
|
+
kind: "reserved";
|
|
50
|
+
runningComputers: number;
|
|
51
|
+
}> | Readonly<{
|
|
52
|
+
kind: "capacity";
|
|
53
|
+
runningComputers: number;
|
|
54
|
+
limit: number;
|
|
55
|
+
retryAfterSeconds: number;
|
|
56
|
+
}> | Readonly<{
|
|
57
|
+
kind: "unavailable";
|
|
58
|
+
cause: unknown;
|
|
59
|
+
}>;
|
|
60
|
+
releaseSlot: (computerId: string) => void;
|
|
61
|
+
runningCount: () => number;
|
|
62
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createFleetInventory } from "./inventory.js";
|
|
2
|
+
import { createFleetBacking } from "./backing.js";
|
|
3
|
+
import { createFleetSlots } from "./slots.js";
|
|
4
|
+
/** Durable collection owner. Host decisions enter as data; projections leave in the result. */
|
|
5
|
+
export function createFleetComputers(owner, capacity) {
|
|
6
|
+
const inventory = createFleetInventory(owner);
|
|
7
|
+
const backing = createFleetBacking(owner);
|
|
8
|
+
const slots = createFleetSlots(owner, capacity);
|
|
9
|
+
return {
|
|
10
|
+
ensure: () => {
|
|
11
|
+
owner.transactionSync(() => {
|
|
12
|
+
inventory.ensure();
|
|
13
|
+
backing.ensure();
|
|
14
|
+
slots.ensure();
|
|
15
|
+
});
|
|
16
|
+
},
|
|
17
|
+
read: inventory.read,
|
|
18
|
+
list: inventory.list,
|
|
19
|
+
count: inventory.count,
|
|
20
|
+
ids: inventory.ids,
|
|
21
|
+
/** A replay preserves the original decision and stages nothing. */
|
|
22
|
+
create: (actor, request, backings) => owner.transactionSync(() => {
|
|
23
|
+
const prior = inventory.replay(actor, request);
|
|
24
|
+
if (prior !== null)
|
|
25
|
+
return { computer: prior, projections: [] };
|
|
26
|
+
const computer = inventory.create(actor, request);
|
|
27
|
+
const projection = backing.computerCreated(computer.computerId, backings);
|
|
28
|
+
return { computer, projections: [projection] };
|
|
29
|
+
}),
|
|
30
|
+
readBacking: (computerId) => inventory.read(computerId) === null ? null : backing.resolve(computerId),
|
|
31
|
+
reserveSlot: slots.reserve,
|
|
32
|
+
releaseSlot: slots.release,
|
|
33
|
+
runningCount: slots.count,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Host-neutral fleet contracts. Host policy is supplied as grants, never imported. */
|
|
2
|
+
export { checkComputerGrant, type ComputerAdmission } from "./admission.js";
|
|
3
|
+
export { createFleetBacking } from "./backing.js";
|
|
4
|
+
export { computerActionSchema, computerProvenanceSchema, computerGrantSchema, computerStateSchema, computerFactSchema, ComputerBackingError, type ComputerAction, type ComputerProvenance, type ComputerGrant, type ComputerRuntimeState, type ComputerFact, type ComputerBackendsProjection, type ComputerBackingChange, type LocalBackendAttachInput, type LocalBackendDetachInput, } from "./wire.js";
|
|
5
|
+
export { ComputerRuntimeUsage } from "./runtime-usage.js";
|
|
6
|
+
export { computerUsageIntervalSchema, type ComputerUsageInterval, } from "./wire.js";
|
|
7
|
+
export { createFleetInventory } from "./inventory.js";
|
|
8
|
+
export { ComputerInventoryError, type FleetActor, type FleetComputer, } from "./inventory-wire.js";
|
|
9
|
+
export { createFleetSlots } from "./slots.js";
|
|
10
|
+
export { FleetLifecycle, FleetGrantRefused, type FleetLifecyclePorts, type FleetLifecycleFact, } from "./lifecycle.js";
|
|
11
|
+
export { createFleetComputers } from "./computers.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Host-neutral fleet contracts. Host policy is supplied as grants, never imported. */
|
|
2
|
+
export { checkComputerGrant } from "./admission.js";
|
|
3
|
+
export { createFleetBacking } from "./backing.js";
|
|
4
|
+
export { computerActionSchema, computerProvenanceSchema, computerGrantSchema, computerStateSchema, computerFactSchema, ComputerBackingError, } from "./wire.js";
|
|
5
|
+
export { ComputerRuntimeUsage } from "./runtime-usage.js";
|
|
6
|
+
export { computerUsageIntervalSchema, } from "./wire.js";
|
|
7
|
+
export { createFleetInventory } from "./inventory.js";
|
|
8
|
+
export { ComputerInventoryError, } from "./inventory-wire.js";
|
|
9
|
+
export { createFleetSlots } from "./slots.js";
|
|
10
|
+
export { FleetLifecycle, FleetGrantRefused, } from "./lifecycle.js";
|
|
11
|
+
export { createFleetComputers } from "./computers.js";
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Opaque host identity; the fleet does not interpret the actor kind. */
|
|
3
|
+
export type FleetActor = Readonly<{
|
|
4
|
+
kind: string;
|
|
5
|
+
id: string;
|
|
6
|
+
}>;
|
|
7
|
+
/** A Computer's public inventory record. */
|
|
8
|
+
export type FleetComputer = Readonly<{
|
|
9
|
+
computerId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
updatedAt: number;
|
|
13
|
+
}>;
|
|
14
|
+
/** A durable create receipt binds the complete request to its actor. */
|
|
15
|
+
export type ComputerCreateRequest = Readonly<{
|
|
16
|
+
name?: string | undefined;
|
|
17
|
+
requestId: string;
|
|
18
|
+
}>;
|
|
19
|
+
/** Inventory pages retain stable ordering and a bounded response. */
|
|
20
|
+
export declare const computerPageSchema: z.ZodObject<{
|
|
21
|
+
limit: z.ZodDefault<z.ZodNumber>;
|
|
22
|
+
offset: z.ZodDefault<z.ZodNumber>;
|
|
23
|
+
}, z.core.$strict>;
|
|
24
|
+
export type ComputerPage = z.input<typeof computerPageSchema>;
|
|
25
|
+
/** Inventory failures, independent of a host's public error mapping. */
|
|
26
|
+
export declare class ComputerInventoryError extends Error {
|
|
27
|
+
readonly code: "partial_init" | "request_conflict" | "name_taken";
|
|
28
|
+
constructor(code: "partial_init" | "request_conflict" | "name_taken");
|
|
29
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Inventory pages retain stable ordering and a bounded response. */
|
|
3
|
+
export const computerPageSchema = z.strictObject({
|
|
4
|
+
limit: z.number().int().min(1).max(100).default(100),
|
|
5
|
+
offset: z.number().int().nonnegative().default(0),
|
|
6
|
+
});
|
|
7
|
+
/** Inventory failures, independent of a host's public error mapping. */
|
|
8
|
+
export class ComputerInventoryError extends Error {
|
|
9
|
+
code;
|
|
10
|
+
constructor(code) {
|
|
11
|
+
super(`Computer inventory refused: ${code}`);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.name = "ComputerInventoryError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
2
|
+
import { type FleetActor, type ComputerCreateRequest, type ComputerPage } from "./inventory-wire.js";
|
|
3
|
+
import type { FleetComputer } from "./inventory-wire.js";
|
|
4
|
+
/** Durable Computer inventory and replay receipts, without host admission policy. */
|
|
5
|
+
export declare function createFleetInventory(options: Readonly<{
|
|
6
|
+
sql: WatchdogSqliteOwner["sql"];
|
|
7
|
+
transactionSync: <T>(body: () => T) => T;
|
|
8
|
+
}>): {
|
|
9
|
+
ensure: () => void;
|
|
10
|
+
replay: (actor: FleetActor, request: ComputerCreateRequest) => FleetComputer | null;
|
|
11
|
+
create: (actor: FleetActor, request: ComputerCreateRequest) => FleetComputer;
|
|
12
|
+
read: (computerId: string) => FleetComputer | null;
|
|
13
|
+
list: (input: ComputerPage) => {
|
|
14
|
+
readonly computers: Readonly<{
|
|
15
|
+
computerId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
updatedAt: number;
|
|
19
|
+
}>[];
|
|
20
|
+
readonly nextOffset: number | null;
|
|
21
|
+
};
|
|
22
|
+
ids: () => string[];
|
|
23
|
+
count: () => number;
|
|
24
|
+
};
|