@dcasegr/skein 0.0.0-stage → 0.1.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 (176) hide show
  1. package/README.md +522 -2
  2. package/bin/skein-host.js +22 -0
  3. package/bin/skein.js +8 -0
  4. package/dist/client/admin-cli.d.ts +10 -0
  5. package/dist/client/admin-cli.js +370 -0
  6. package/dist/client/admin.d.ts +139 -0
  7. package/dist/client/admin.js +256 -0
  8. package/dist/client/bundle.d.ts +9 -0
  9. package/dist/client/bundle.js +34 -0
  10. package/dist/client/cli.d.ts +44 -0
  11. package/dist/client/cli.js +306 -0
  12. package/dist/client/client.d.ts +161 -0
  13. package/dist/client/client.js +237 -0
  14. package/dist/client/config.d.ts +18 -0
  15. package/dist/client/config.js +31 -0
  16. package/dist/client/conversation.d.ts +62 -0
  17. package/dist/client/conversation.js +113 -0
  18. package/dist/client/raw.d.ts +94 -0
  19. package/dist/client/raw.js +154 -0
  20. package/dist/client/target.d.ts +34 -0
  21. package/dist/client/target.js +158 -0
  22. package/dist/dev/cli.d.ts +8 -0
  23. package/dist/dev/cli.js +248 -0
  24. package/dist/dev/scan.d.ts +15 -0
  25. package/dist/dev/scan.js +173 -0
  26. package/dist/envelope-cbor.d.ts +110 -0
  27. package/dist/envelope-cbor.js +160 -0
  28. package/dist/envelope.d.ts +38 -0
  29. package/dist/envelope.js +101 -0
  30. package/dist/host/arc.d.ts +131 -0
  31. package/dist/host/arc.js +425 -0
  32. package/dist/host/billing.d.ts +102 -0
  33. package/dist/host/billing.js +167 -0
  34. package/dist/host/boot.d.ts +246 -0
  35. package/dist/host/boot.js +643 -0
  36. package/dist/host/cli.d.ts +57 -0
  37. package/dist/host/cli.js +1253 -0
  38. package/dist/host/clock.d.ts +3 -0
  39. package/dist/host/clock.js +8 -0
  40. package/dist/host/control.d.ts +45 -0
  41. package/dist/host/control.js +170 -0
  42. package/dist/host/cron.d.ts +99 -0
  43. package/dist/host/cron.js +196 -0
  44. package/dist/host/deliver.d.ts +53 -0
  45. package/dist/host/deliver.js +251 -0
  46. package/dist/host/deploy.d.ts +29 -0
  47. package/dist/host/deploy.js +55 -0
  48. package/dist/host/door-answer.d.ts +24 -0
  49. package/dist/host/door-answer.js +21 -0
  50. package/dist/host/events.d.ts +89 -0
  51. package/dist/host/events.js +222 -0
  52. package/dist/host/fake-arcade.d.ts +40 -0
  53. package/dist/host/fake-arcade.js +149 -0
  54. package/dist/host/fake-discovery.d.ts +4 -0
  55. package/dist/host/fake-discovery.js +58 -0
  56. package/dist/host/feeds.d.ts +127 -0
  57. package/dist/host/feeds.js +422 -0
  58. package/dist/host/frontdoor.d.ts +92 -0
  59. package/dist/host/frontdoor.js +172 -0
  60. package/dist/host/genesis.d.ts +259 -0
  61. package/dist/host/genesis.js +353 -0
  62. package/dist/host/handles.d.ts +40 -0
  63. package/dist/host/handles.js +88 -0
  64. package/dist/host/hostenv.d.ts +30 -0
  65. package/dist/host/hostenv.js +83 -0
  66. package/dist/host/image-chain.d.ts +127 -0
  67. package/dist/host/image-chain.js +440 -0
  68. package/dist/host/install.d.ts +22 -0
  69. package/dist/host/install.js +139 -0
  70. package/dist/host/instances.d.ts +227 -0
  71. package/dist/host/instances.js +430 -0
  72. package/dist/host/kernel.d.ts +246 -0
  73. package/dist/host/kernel.js +603 -0
  74. package/dist/host/liveness.d.ts +65 -0
  75. package/dist/host/liveness.js +155 -0
  76. package/dist/host/manifest.d.ts +160 -0
  77. package/dist/host/manifest.js +597 -0
  78. package/dist/host/p2p.d.ts +337 -0
  79. package/dist/host/p2p.js +1002 -0
  80. package/dist/host/packet.d.ts +121 -0
  81. package/dist/host/packet.js +562 -0
  82. package/dist/host/plan.d.ts +154 -0
  83. package/dist/host/plan.js +418 -0
  84. package/dist/host/providers.d.ts +238 -0
  85. package/dist/host/providers.js +565 -0
  86. package/dist/host/roster.d.ts +72 -0
  87. package/dist/host/roster.js +172 -0
  88. package/dist/host/router.d.ts +773 -0
  89. package/dist/host/router.js +2389 -0
  90. package/dist/host/signer.d.ts +49 -0
  91. package/dist/host/signer.js +167 -0
  92. package/dist/host/store-sqlite.d.ts +39 -0
  93. package/dist/host/store-sqlite.js +170 -0
  94. package/dist/host/stores.d.ts +43 -0
  95. package/dist/host/stores.js +342 -0
  96. package/dist/host/sync.d.ts +121 -0
  97. package/dist/host/sync.js +467 -0
  98. package/dist/host/testbeef.d.ts +21 -0
  99. package/dist/host/testbeef.js +37 -0
  100. package/dist/host/testhost.d.ts +149 -0
  101. package/dist/host/testhost.js +211 -0
  102. package/dist/host/vcdiff.d.ts +9 -0
  103. package/dist/host/vcdiff.js +210 -0
  104. package/dist/host/wasm-kernel.d.ts +87 -0
  105. package/dist/host/wasm-kernel.js +296 -0
  106. package/dist/host/wasm-worker.d.ts +1 -0
  107. package/dist/host/wasm-worker.js +186 -0
  108. package/dist/peers/cron.d.ts +33 -0
  109. package/dist/peers/cron.js +75 -0
  110. package/dist/peers/infer-main.d.ts +2 -0
  111. package/dist/peers/infer-main.js +74 -0
  112. package/dist/peers/infer.d.ts +163 -0
  113. package/dist/peers/infer.js +367 -0
  114. package/dist/runtime/beef.d.ts +28 -0
  115. package/dist/runtime/beef.js +107 -0
  116. package/dist/runtime/bitcoin.d.ts +36 -0
  117. package/dist/runtime/bitcoin.js +141 -0
  118. package/dist/runtime/cid.d.ts +14 -0
  119. package/dist/runtime/cid.js +51 -0
  120. package/dist/runtime/dispatch.d.ts +92 -0
  121. package/dist/runtime/dispatch.js +142 -0
  122. package/dist/runtime/envelope.d.ts +60 -0
  123. package/dist/runtime/envelope.js +101 -0
  124. package/dist/runtime/heads.d.ts +25 -0
  125. package/dist/runtime/heads.js +37 -0
  126. package/dist/runtime/identity.d.ts +28 -0
  127. package/dist/runtime/identity.js +67 -0
  128. package/dist/runtime/index-store.d.ts +152 -0
  129. package/dist/runtime/index-store.js +715 -0
  130. package/dist/runtime/log.d.ts +62 -0
  131. package/dist/runtime/log.js +104 -0
  132. package/dist/runtime/programs.d.ts +30 -0
  133. package/dist/runtime/programs.js +61 -0
  134. package/dist/runtime/records.d.ts +135 -0
  135. package/dist/runtime/records.js +149 -0
  136. package/dist/runtime/sqlite.d.ts +33 -0
  137. package/dist/runtime/sqlite.js +604 -0
  138. package/dist/runtime/store.d.ts +141 -0
  139. package/dist/runtime/store.js +24 -0
  140. package/dist/runtime/syscalls.d.ts +9 -0
  141. package/dist/runtime/syscalls.js +41 -0
  142. package/dist/runtime/tree.d.ts +60 -0
  143. package/dist/runtime/tree.js +196 -0
  144. package/dist/runtime/types.d.ts +100 -0
  145. package/dist/runtime/types.js +7 -0
  146. package/dist/testapps.d.ts +101 -0
  147. package/dist/testapps.js +275 -0
  148. package/dist/testkit.d.ts +25 -0
  149. package/dist/testkit.js +48 -0
  150. package/dist/wallet.d.ts +35 -0
  151. package/dist/wallet.js +87 -0
  152. package/images/README.md +68 -0
  153. package/images/default/README.md +38 -0
  154. package/images/default/bin/frontdoor.cid +1 -0
  155. package/images/default/bin/frontdoor.json +5 -0
  156. package/images/default/bin/messagebox.cid +1 -0
  157. package/images/default/bin/messagebox.json +8 -0
  158. package/images/default/bin/wallet.cid +1 -0
  159. package/images/default/bin/wallet.json +14 -0
  160. package/images/default/etc/config.json +1 -0
  161. package/images/default/etc/dispatch.json +82 -0
  162. package/images/host/etc/apps.json +8 -0
  163. package/kernel-zig/README.md +1247 -0
  164. package/kernel-zig/web/client.js +65 -0
  165. package/kernel-zig/web/kernel.d.ts +59 -0
  166. package/kernel-zig/web/kernel.js +329 -0
  167. package/kernel-zig/web/store.js +67 -0
  168. package/kernel-zig/web/worker.js +79 -0
  169. package/kernel-zig/zig-out/web/skein-kernel.wasm +0 -0
  170. package/package.json +77 -4
  171. package/wasm/README.md +35 -0
  172. package/wasm/frontdoor.wasm +0 -0
  173. package/wasm/messagebox.wasm +0 -0
  174. package/wasm/resolve.wasm +0 -0
  175. package/wasm/wallet.wasm +0 -0
  176. package/wasm/wire-probe.wasm +0 -0
@@ -0,0 +1,154 @@
1
+ import type { CID } from "multiformats/cid";
2
+ import { type Rec } from "../client/bundle.ts";
3
+ import { type DispatchRow } from "../runtime/dispatch.ts";
4
+ import type { Store } from "../runtime/store.ts";
5
+ import type { Objects } from "./boot.ts";
6
+ import type { AddressEntry } from "./deploy.ts";
7
+ import { type Checked, type Derived, type ShellSource } from "./manifest.ts";
8
+ /** What the plan reads of the instance's store: blocks by CID. */
9
+ export type ViewStore = Pick<Store, "get" | "has" | "bytes" | "putBlock">;
10
+ /** A file of an app's tree by its path ("bin/x.wasm"), or undefined: the install reads nothing else. */
11
+ export type ReadFile = (rel: string) => Uint8Array | undefined;
12
+ /**
13
+ * An app's tree: its root, the git objects to send (none for a tree the
14
+ * instance holds), its manifest checked, and its files as the install reads
15
+ * them.
16
+ */
17
+ export interface AppTree {
18
+ root: CID;
19
+ records: Rec[];
20
+ checked: Checked;
21
+ read: ReadFile;
22
+ }
23
+ export declare function parseManifest(text: string): unknown;
24
+ /**
25
+ * Read and check the app whose tree the instance's store holds (#91: the
26
+ * git app's clone; reads are open by CID). Only the files the install reads
27
+ * are fetched out of the store: the manifest, and what its programs name.
28
+ */
29
+ export declare function readStoredApp(store: Pick<Store, "bytes" | "has" | "putBlock">, tree: CID): Promise<AppTree>;
30
+ /** What the install reads of the instance (its store, read only). */
31
+ export interface InstanceView {
32
+ store: ViewStore;
33
+ /** The instance's identity (hex), and programs (name → program record). */
34
+ identity: string;
35
+ programs: Record<string, CID>;
36
+ addressBook: AddressEntry[];
37
+ /** Every head and its root (the store's index). */
38
+ heads: Array<{
39
+ name: string;
40
+ root: CID;
41
+ }>;
42
+ /** The route table now (#77): a route with `app` was installed by that app; without, it is the genesis's or root's own. */
43
+ dispatch: DispatchRow[];
44
+ }
45
+ /** The app's root head (#77): `<name>/app`. */
46
+ export declare const appHead: (name: string) => string;
47
+ /** The app record the view's head `<name>/app` names, if it is one. */
48
+ export declare function appRecordIn(view: Pick<InstanceView, "store" | "heads">, name: string): Promise<{
49
+ cid: CID;
50
+ record: AppRecord;
51
+ } | undefined>;
52
+ /** The app record (a head's root): the manifest as installed. */
53
+ export type AppRecord = Omit<Checked["manifest"], "programs"> & {
54
+ programs: Record<string, CID>;
55
+ tree: CID;
56
+ state?: CID;
57
+ };
58
+ /** A manifest's `config` with `over` merged in: per program, its keys over the manifest's. */
59
+ export declare function mergeConfig(base: Record<string, unknown> | undefined, over: Record<string, unknown>): Record<string, unknown>;
60
+ /** A route change the install sends: the route as the kernel takes it (filters named in full, program a CID, address as served, `app`), and the role it runs. */
61
+ export interface RowOp {
62
+ op: "add" | "remove";
63
+ row: DispatchRow & {
64
+ app?: string;
65
+ };
66
+ role?: string;
67
+ }
68
+ export interface HeadOp {
69
+ name: string;
70
+ tree: CID;
71
+ }
72
+ /** What an install sends, in order, and what it shows. */
73
+ export interface Plan {
74
+ app: string;
75
+ version: string;
76
+ record: AppRecord;
77
+ recordCid: CID;
78
+ /** The records to send (the store does not have them), in order: objects, modules, program records, the app record. */
79
+ records: Rec[];
80
+ heads: HeadOp[];
81
+ rows: RowOp[];
82
+ start?: Record<string, unknown>;
83
+ /** An install over an earlier version. */
84
+ upgrade?: string;
85
+ /** For the prompt only. */
86
+ requires: string[];
87
+ publishes: string[];
88
+ /** What `config.overlay` added (APPS.md §6). */
89
+ derived: Derived;
90
+ notes: string[];
91
+ }
92
+ /**
93
+ * The routes an app record asks for, resolved against the instance (#143): each as the kernel
94
+ * takes it — the address as served, the filters named in full (an own filter `<app>.<name>`, the
95
+ * kernel's and another app's as written), the handler's role's program record and its `fn`, and
96
+ * `app`; a read route (no handler) has no program. Another app's filter must be one that app's
97
+ * installed record declares.
98
+ */
99
+ export declare function wiring(record: AppRecord, view: InstanceView): Promise<RowOp[]>;
100
+ /** The inputs of a shell program record (the kernel's shell thread takes {cmd, tree, cwd?, env?}). */
101
+ export declare const SHELL_INPUTS: {
102
+ readonly cmd: "string";
103
+ readonly tree: "cid";
104
+ readonly cwd: "string?";
105
+ readonly env: "map?";
106
+ };
107
+ /**
108
+ * A shell program's record (#83; docs/APPS.md "A shell program") from the
109
+ * manifest's form, and the raw blocks it names: `{kind: "program", name:
110
+ * <role>, code: {ts: "shell"}, modules: {<command>: <raw module CID>},
111
+ * support: {<command>: {mount, files: {<path>: <raw CID>}, env}}, inputs,
112
+ * services: [], description, app}` — the shape the kernel's shell runs
113
+ * (kernel-zig/src/shell.zig loadModules), each module and support file a raw
114
+ * block of the tree's file.
115
+ */
116
+ export declare function shellProgram(read: ReadFile, role: string, src: ShellSource, app: string): {
117
+ record: Record<string, unknown>;
118
+ blocks: Rec[];
119
+ };
120
+ /**
121
+ * Plan the install of `t` into the instance `view` reads: every check (APPS.md §3 step 0), then what to send.
122
+ * An image's install at birth (#141, boot.ts) is the same plan: its routes go into the genesis.
123
+ */
124
+ export declare function planInstall(t: AppTree, view: InstanceView, o: {
125
+ modules: Objects;
126
+ }): Promise<Plan>;
127
+ /** The permission prompt: what the app asks for (APPS.md §3) — its routes, filters and roles read aloud. */
128
+ export declare function describe(p: Plan, record?: AppRecord): string[];
129
+ /** Send a plan as root: `send(box, body)` is root's message to the instance. */
130
+ export declare function sendInstall(p: Plan, send: (box: string, body: Uint8Array) => Promise<unknown>): Promise<{
131
+ messages: number;
132
+ }>;
133
+ export interface UninstallPlan {
134
+ app: string;
135
+ record: AppRecord;
136
+ stop?: Record<string, unknown>;
137
+ rows: RowOp[];
138
+ }
139
+ /** What uninstalling `name` sends: `stop`, then its routes removed (every route the table holds with `app: name`). The app's heads are left. */
140
+ export declare function planUninstall(name: string, view: InstanceView): Promise<UninstallPlan>;
141
+ export declare function sendUninstall(p: UninstallPlan, send: (box: string, body: Uint8Array) => Promise<unknown>): Promise<{
142
+ messages: number;
143
+ }>;
144
+ /**
145
+ * Root's own route (#143; `skein routes`): one `dispatch` add or remove of a route with no `app`
146
+ * — an app's upgrade or uninstall leaves it. The site at the instance's root is one: a read route
147
+ * {transport: "http", address: "/", prefix: true, filters: ["site.get"], root: "www"} (the site's
148
+ * filter answering every path no longer route takes). Refused at a key another route has; a filter
149
+ * of an app must be one its installed record declares.
150
+ */
151
+ export declare function planRootRoute(view: InstanceView, op: "add" | "remove", row: DispatchRow): Promise<{
152
+ prompt: string[];
153
+ rows: RowOp[];
154
+ }>;
@@ -0,0 +1,418 @@
1
+ // The install's plan (#72, #77, #91, #143; docs/APPS.md §3): what installing or
2
+ // uninstalling an app sends, as root, to the kernel's admin boxes — objects,
3
+ // the head, the routes; no key (#143: "the person who's doing the deploying is
4
+ // root and doesn't need to grant themselves anything") — read from a view of
5
+ // the instance: its heads, route table, address book, genesis programs, and
6
+ // its store by CID. No node APIs: the node client
7
+ // (install.ts and src/client/admin.ts: `skein install`, #124, a store file's or the explorer's view) and the management
8
+ // site (shruggr/skein-site, #92: a view over the instance's explorer reads, in
9
+ // a browser) plan with the same code. install.ts's header has the steps.
10
+ import * as dagCbor from "@ipld/dag-cbor";
11
+ import { chunk } from "../client/bundle.js";
12
+ import { encode, parse as parseCid } from "../runtime/cid.js";
13
+ import { rowKey as kernelRowKey } from "../runtime/dispatch.js";
14
+ import { programRecord, RAW, rawCid, wasmKind } from "../runtime/programs.js";
15
+ import { readBlob, readTree } from "../runtime/tree.js";
16
+ import { checkManifest, filterRef, handlerOf, missingInterfaces, rowAddress, routeKey } from "./manifest.js";
17
+ const textOf = (b) => new TextDecoder().decode(b);
18
+ export function parseManifest(text) {
19
+ try {
20
+ return JSON.parse(text);
21
+ }
22
+ catch (e) {
23
+ throw new Error(`etc/app.json: ${e.message}`);
24
+ }
25
+ }
26
+ /**
27
+ * Read and check the app whose tree the instance's store holds (#91: the
28
+ * git app's clone; reads are open by CID). Only the files the install reads
29
+ * are fetched out of the store: the manifest, and what its programs name.
30
+ */
31
+ export async function readStoredApp(store, tree) {
32
+ const files = new Map();
33
+ const walk = async (t, at) => {
34
+ for (const e of await readTree(store, t)) {
35
+ const path = at ? `${at}/${e.name}` : e.name;
36
+ if (e.mode === "40000")
37
+ await walk(e.cid, path);
38
+ else if (e.mode === "100644" || e.mode === "100755")
39
+ files.set(path, e.cid);
40
+ }
41
+ };
42
+ await walk(tree, "");
43
+ const m = files.get("etc/app.json");
44
+ if (!m)
45
+ throw new Error(`${tree}: no etc/app.json (an app is a tree with a manifest: docs/APPS.md §2)`);
46
+ const checked = checkManifest(parseManifest(new TextDecoder().decode(await readBlob(store, m))), (rel) => files.has(rel));
47
+ const wanted = new Set();
48
+ for (const src of Object.values(checked.sources)) {
49
+ if (src.kind === "wasm" || src.kind === "cid") {
50
+ wanted.add(src.path);
51
+ wanted.add(`bin/${src.name}.json`);
52
+ }
53
+ if (src.kind === "shell") {
54
+ for (const p of Object.values(src.modules))
55
+ wanted.add(p);
56
+ for (const x of Object.values(src.support))
57
+ for (const p of Object.values(x.files))
58
+ wanted.add(p);
59
+ }
60
+ }
61
+ const bytes = new Map();
62
+ for (const p of wanted) {
63
+ const c = files.get(p);
64
+ if (c)
65
+ bytes.set(p, await readBlob(store, c));
66
+ }
67
+ return { root: tree, records: [], checked, read: (rel) => bytes.get(rel) };
68
+ }
69
+ /** The app's root head (#77): `<name>/app`. */
70
+ export const appHead = (name) => `${name}/app`;
71
+ /** The app record the view's head `<name>/app` names, if it is one. */
72
+ export async function appRecordIn(view, name) {
73
+ const root = view.heads.find((h) => h.name === appHead(name))?.root;
74
+ if (!root)
75
+ return undefined;
76
+ const r = await view.store.get(root).catch(() => undefined);
77
+ return r && r.kind === "app" ? { cid: root, record: r } : undefined;
78
+ }
79
+ // ---------------------------------------------------------------- the plan
80
+ /** A manifest's `config` with `over` merged in: per program, its keys over the manifest's. */
81
+ export function mergeConfig(base, over) {
82
+ const out = { ...base };
83
+ for (const [k, x] of Object.entries(over)) {
84
+ const b = out[k];
85
+ out[k] = x && typeof x === "object" && !Array.isArray(x) && b && typeof b === "object" && !Array.isArray(b) ? { ...b, ...x } : x;
86
+ }
87
+ return out;
88
+ }
89
+ /**
90
+ * The routes an app record asks for, resolved against the instance (#143): each as the kernel
91
+ * takes it — the address as served, the filters named in full (an own filter `<app>.<name>`, the
92
+ * kernel's and another app's as written), the handler's role's program record and its `fn`, and
93
+ * `app`; a read route (no handler) has no program. Another app's filter must be one that app's
94
+ * installed record declares.
95
+ */
96
+ export async function wiring(record, view) {
97
+ const out = [];
98
+ const roles = Object.keys(record.programs);
99
+ for (const r of record.routes) {
100
+ const { transport, address, prefix, filters, handler, ...settings } = r;
101
+ const full = (filters ?? []).map((f) => filterRef(record.name, f));
102
+ for (const f of full) {
103
+ const dot = f.lastIndexOf(".");
104
+ const app = f.slice(0, dot), name = f.slice(dot + 1);
105
+ if (app === "kernel" || app === record.name)
106
+ continue;
107
+ const other = await appRecordIn(view, app);
108
+ if (!other)
109
+ throw new Error(`route ${routeKey(record.name, r)}: filter ${f}: no app ${app} is installed`);
110
+ if (!other.record.filters || !(name in other.record.filters))
111
+ throw new Error(`route ${routeKey(record.name, r)}: filter ${f}: app ${app} declares no filter ${name}`);
112
+ }
113
+ const base = { ...settings, transport, address: rowAddress(record.name, r), ...(prefix ? { prefix: true } : {}), ...(full.length ? { filters: full } : {}) };
114
+ if (handler === undefined) {
115
+ out.push({ op: "add", row: { ...base, app: record.name } });
116
+ continue;
117
+ }
118
+ const h = handlerOf(handler, roles);
119
+ const cid = h && record.programs[h.role];
120
+ if (!h || !cid)
121
+ throw new Error(`route ${routeKey(record.name, r)}: handler ${handler}: no program for it`);
122
+ out.push({ op: "add", row: { ...base, program: cid, ...(h.fn ? { fn: h.fn } : {}), app: record.name }, role: h.role });
123
+ }
124
+ return out;
125
+ }
126
+ /** A route's key as the kernel's table knows it (the resolved route). */
127
+ const keyOf = (r) => kernelRowKey(r);
128
+ /** The fields of a route that are not its settings (its key, filters, program, `fn`; `app` the install's). */
129
+ const ROW_CORE = new Set(["transport", "address", "prefix", "filters", "program", "fn", "handler", "app"]);
130
+ /** A route's settings as the prompt shows them (` (root www)`), or "". */
131
+ const showSettings = (r) => {
132
+ const s = Object.entries(r).filter(([k, v]) => !ROW_CORE.has(k) && v !== undefined).map(([k, v]) => `${k} ${typeof v === "string" ? v : JSON.stringify(v)}`);
133
+ return s.length ? ` (${s.join(", ")})` : "";
134
+ };
135
+ /** A route's filters as the prompt shows them: ` [kernel.brc104, amm.quote]`, or "". */
136
+ const showFilters = (fs) => Array.isArray(fs) && fs.length ? ` [${fs.join(", ")}]` : "";
137
+ /** A route as one line of the prompt: `<transport> <address>[ prefix] [<filters>] → <role>[.<fn>] | (a read: its filters answer)[ (<settings>)]`. */
138
+ const showRow = (r) => `${r.row.transport} ${r.row.address}${r.row.prefix ? " prefix" : ""}${showFilters(r.row.filters)} → ${r.row.program === undefined ? "(a read: its filters answer)" : `${r.role ?? String(r.row.program)}${r.row.fn ? `.${r.row.fn}` : ""}`}${showSettings(r.row)}`;
139
+ /** The program records of the tree's programs, the modules to send, and the role → record map. */
140
+ async function programsOf(t, view, extra) {
141
+ const programs = {};
142
+ const records = [];
143
+ for (const [role, src] of Object.entries(t.checked.sources)) {
144
+ if (src.kind === "shell") {
145
+ const { record, blocks } = shellProgram(t.read, role, src, t.checked.manifest.name);
146
+ records.push(...blocks);
147
+ const b = encode(record);
148
+ programs[role] = b.cid;
149
+ records.push({ cid: b.cid, bytes: b.bytes });
150
+ continue;
151
+ }
152
+ if (src.kind === "instance") {
153
+ const p = view.programs[src.name];
154
+ if (!p)
155
+ throw new Error(`programs.${role}: the instance has no program ${src.name} (its genesis names ${Object.keys(view.programs).join(", ")})`);
156
+ programs[role] = p;
157
+ continue;
158
+ }
159
+ let module;
160
+ const file = (path) => { const b = t.read(path); if (!b)
161
+ throw new Error(`${path}: not in the tree`); return b; };
162
+ if (src.kind === "wasm") {
163
+ const bytes = file(src.path);
164
+ if (!wasmKind(bytes))
165
+ throw new Error(`${src.path}: not a wasm module or component`);
166
+ module = rawCid(bytes);
167
+ records.push({ cid: module, bytes });
168
+ }
169
+ else {
170
+ const text = new TextDecoder().decode(file(src.path)).trim();
171
+ try {
172
+ module = parseCid(text);
173
+ }
174
+ catch {
175
+ throw new Error(`${src.path}: not a CID: ${JSON.stringify(text)}`);
176
+ }
177
+ if (module.code !== RAW)
178
+ throw new Error(`${src.path}: ${module} is not a raw module CID`);
179
+ if (!(await view.store.has(module))) {
180
+ const bytes = await extra.get(module);
181
+ if (!bytes)
182
+ throw new Error(`${src.path}: names ${module}, which neither the instance nor this host holds`);
183
+ records.push({ cid: module, bytes });
184
+ }
185
+ }
186
+ const metaFile = t.read(`bin/${src.name}.json`);
187
+ const meta = metaFile ? JSON.parse(new TextDecoder().decode(metaFile)) : {};
188
+ // `app`: the program knows its app (its heads under `<app>/`) from its own record (APPS.md §2).
189
+ const b = encode({ ...programRecord(src.name, module, meta), app: t.checked.manifest.name });
190
+ programs[role] = b.cid;
191
+ records.push({ cid: b.cid, bytes: b.bytes });
192
+ }
193
+ return { programs, records };
194
+ }
195
+ /** The inputs of a shell program record (the kernel's shell thread takes {cmd, tree, cwd?, env?}). */
196
+ export const SHELL_INPUTS = { cmd: "string", tree: "cid", cwd: "string?", env: "map?" };
197
+ const SHELL_DESCRIPTION = "Run a bash command in the wasm shell over a tree; result {exitCode, stdout, stderr, tree}.";
198
+ /**
199
+ * A shell program's record (#83; docs/APPS.md "A shell program") from the
200
+ * manifest's form, and the raw blocks it names: `{kind: "program", name:
201
+ * <role>, code: {ts: "shell"}, modules: {<command>: <raw module CID>},
202
+ * support: {<command>: {mount, files: {<path>: <raw CID>}, env}}, inputs,
203
+ * services: [], description, app}` — the shape the kernel's shell runs
204
+ * (kernel-zig/src/shell.zig loadModules), each module and support file a raw
205
+ * block of the tree's file.
206
+ */
207
+ export function shellProgram(read, role, src, app) {
208
+ const blocks = new Map();
209
+ const raw = (path, module) => {
210
+ const bytes = read(path);
211
+ if (!bytes)
212
+ throw new Error(`${path}: not in the tree`);
213
+ if (module && wasmKind(bytes) !== "module")
214
+ throw new Error(`${path}: not a wasm module (a shell program runs WASI preview1 modules)`);
215
+ const cid = rawCid(bytes);
216
+ blocks.set(cid.toString(), { cid, bytes });
217
+ return cid;
218
+ };
219
+ const modules = Object.fromEntries(Object.entries(src.modules).map(([cmd, path]) => [cmd, raw(path, true)]));
220
+ const support = Object.fromEntries(Object.entries(src.support).map(([cmd, s]) => [cmd, {
221
+ mount: s.mount, files: Object.fromEntries(Object.entries(s.files).map(([under, path]) => [under, raw(path, false)])), env: s.env,
222
+ }]));
223
+ return {
224
+ record: { kind: "program", name: role, code: { ts: "shell" }, modules, support, inputs: SHELL_INPUTS, services: [], description: src.description ?? SHELL_DESCRIPTION, app },
225
+ blocks: [...blocks.values()],
226
+ };
227
+ }
228
+ /**
229
+ * Plan the install of `t` into the instance `view` reads: every check (APPS.md §3 step 0), then what to send.
230
+ * An image's install at birth (#141, boot.ts) is the same plan: its routes go into the genesis.
231
+ */
232
+ export async function planInstall(t, view, o) {
233
+ const m = t.checked.manifest;
234
+ const notes = [];
235
+ // requires: every interface is provided by some installed app (each app head's root record).
236
+ const installed = [];
237
+ for (const h of view.heads) {
238
+ if (!h.name.endsWith("/app") || h.name === appHead(m.name))
239
+ continue;
240
+ const r = await view.store.get(h.root).catch(() => undefined);
241
+ if (r?.kind === "app")
242
+ installed.push({ name: h.name.slice(0, -"/app".length), provides: r.provides });
243
+ }
244
+ const missing = missingInterfaces(m.requires, installed);
245
+ if (missing.length)
246
+ throw new Error(`requires ${missing.join(", ")}: no installed app provides ${missing.length > 1 ? "them" : "it"}`);
247
+ // The name is identity (#77): its root head is an app record or nothing.
248
+ const before = await appRecordIn(view, m.name);
249
+ if (!before && view.heads.some((h) => h.name === appHead(m.name)))
250
+ throw new Error(`head ${appHead(m.name)} exists and its root is not an app record: another use of the name`);
251
+ const { programs, records: progRecords } = await programsOf(t, view, o.modules);
252
+ const { programs: _paths, ...rest } = m;
253
+ void _paths;
254
+ const record = JSON.parse(JSON.stringify({ ...rest, programs: {}, tree: null }));
255
+ record.programs = programs;
256
+ record.tree = t.root;
257
+ if (before?.record.state)
258
+ record.state = before.record.state;
259
+ const app = encode(record);
260
+ const want = await wiring(record, view);
261
+ // No route may take a key the genesis, root or another app has.
262
+ const taken = new Map();
263
+ for (const r of view.dispatch)
264
+ if (r.app !== m.name)
265
+ taken.set(keyOf(r), r.app ? `app ${r.app}` : "the genesis (or root's own)");
266
+ for (const r of want) {
267
+ const who = taken.get(keyOf(r.row));
268
+ if (who)
269
+ throw new Error(`route ${keyOf(r.row)}: ${who} has it`);
270
+ }
271
+ // An earlier version: what it had and this one has not is removed.
272
+ const rows = [...want];
273
+ if (before) {
274
+ const keep = new Set(want.map((r) => keyOf(r.row)));
275
+ for (const r of view.dispatch)
276
+ if (r.app === m.name && !keep.has(keyOf(r)))
277
+ rows.push({ op: "remove", row: r });
278
+ }
279
+ // Only what changes: a row the instance has as asked is not sent again.
280
+ const have = new Map(view.dispatch.map((r) => [keyOf(r), r]));
281
+ const sendRows = rows.filter((r) => {
282
+ const cur = have.get(keyOf(r.row));
283
+ if (r.op === "remove")
284
+ return !!cur;
285
+ return !cur || !sameRow(cur, r.row);
286
+ });
287
+ // Records the store has are not sent again.
288
+ const all = [];
289
+ const seen = new Set();
290
+ const trees = t.records.filter((r) => textOf(r.bytes.subarray(0, 5)) === "tree ");
291
+ const blobs = t.records.filter((r) => textOf(r.bytes.subarray(0, 5)) !== "tree ");
292
+ // blobs, then trees (children before parents: the root last), modules and program records, the app record.
293
+ const ordered = [...blobs, ...trees.filter((r) => !r.cid.equals(t.root)), ...t.records.filter((r) => r.cid.equals(t.root)), ...progRecords, { cid: app.cid, bytes: app.bytes }];
294
+ for (const r of ordered) {
295
+ const k = r.cid.toString();
296
+ if (seen.has(k))
297
+ continue;
298
+ seen.add(k);
299
+ if (!(await view.store.has(r.cid)))
300
+ all.push(r);
301
+ }
302
+ const heads = [{ name: appHead(m.name), tree: app.cid }];
303
+ const publishes = gossipOf(m.config);
304
+ return {
305
+ app: m.name, version: m.version, record, recordCid: app.cid, records: all, heads,
306
+ rows: sendRows, ...(m.start ? { start: m.start.body } : {}),
307
+ ...(before ? { upgrade: before.record.version } : {}), requires: m.requires, publishes, derived: t.checked.derived, notes,
308
+ };
309
+ }
310
+ const sameRow = (a, b) => encode(a).cid.equals(encode(b).cid);
311
+ /** What an overlay app publishes (APPS.md §6), for information: `<topic>`, `-admit`, `-proof` per topic not turned off. */
312
+ function gossipOf(config) {
313
+ const ov = config?.overlay;
314
+ if (!ov?.topics)
315
+ return [];
316
+ return Object.keys(ov.topics).filter((t) => ov.gossip?.[t] !== false).flatMap((t) => [t, `${t}-admit`, `${t}-proof`]);
317
+ }
318
+ /** The permission prompt: what the app asks for (APPS.md §3) — its routes, filters and roles read aloud. */
319
+ export function describe(p, record = p.record) {
320
+ const out = [];
321
+ out.push(`${p.upgrade ? "upgrade" : "install"} ${p.app} ${p.version}${p.upgrade ? ` (installed: ${p.upgrade})` : ""}${record.description ? ` — ${record.description}` : ""}`);
322
+ const d = p.derived ?? { routes: [] };
323
+ const from = (yes) => (yes ? " (derived: config.overlay)" : "");
324
+ for (const h of p.heads)
325
+ out.push(` head ${h.name} → the app record ${p.recordCid} (tree ${record.tree})`);
326
+ for (const r of record.routes) {
327
+ const k = routeKey(record.name, r);
328
+ out.push(` route ${r.transport} ${rowAddress(record.name, r)}${r.prefix ? " prefix" : ""}${showFilters((r.filters ?? []).map((f) => filterRef(record.name, f)))} → ${r.handler ?? "(a read: its filters answer, nothing logged)"}${showSettings(r)}${from(d.routes.includes(k))}`);
329
+ }
330
+ for (const [f, h] of Object.entries(record.filters ?? {}))
331
+ out.push(` filter ${record.name}.${f} → ${h} (any route may list it; runs before anything is recorded)`);
332
+ for (const [role, fns] of Object.entries(record.roles ?? {}))
333
+ out.push(` role ${role === "root" || role === "user" ? role : `${record.name}.${role}`} gates ${fns.join(", ")}`);
334
+ if (record.start)
335
+ out.push(` start ${JSON.stringify(record.start.body)} into ${p.app}`);
336
+ if (record.stop)
337
+ out.push(` stop ${JSON.stringify(record.stop.body)} (at uninstall)`);
338
+ if (p.requires.length)
339
+ out.push(` requires ${p.requires.join(", ")}`);
340
+ for (const pr of record.provides)
341
+ out.push(` provides ${pr.interface}: ${Object.entries(pr.functions).map(([f, x]) => `${f}${x.writes ? "" : " (read)"}`).join(", ")}`);
342
+ if (p.publishes.length)
343
+ out.push(` publishes ${p.publishes.join(", ")} (for information: emitting needs no grant)`);
344
+ for (const n of p.notes)
345
+ out.push(` note ${n}`);
346
+ out.push(` messages objects ×${Math.max(1, [...chunk(p.records)].length)} (${p.records.length} records) · head ×${p.heads.length} · dispatch ×${p.rows.length}${p.start ? " · start" : ""}`);
347
+ for (const r of p.rows)
348
+ out.push(` dispatch ${r.op} ${showRow(r)}`);
349
+ return out;
350
+ }
351
+ /** Send a plan as root: `send(box, body)` is root's message to the instance. */
352
+ export async function sendInstall(p, send) {
353
+ let n = 0;
354
+ const go = async (box, body) => { await send(box, body); n++; };
355
+ for (const b of chunk(p.records))
356
+ await go("objects", b);
357
+ for (const h of p.heads)
358
+ await go("head", dagCbor.encode({ name: h.name, tree: h.tree }));
359
+ // Removes first: an upgrade frees what it no longer asks for.
360
+ for (const r of [...p.rows].sort((a, b) => (a.op === b.op ? 0 : a.op === "remove" ? -1 : 1)))
361
+ await go("dispatch", dagCbor.encode(rowBody(r)));
362
+ if (p.start)
363
+ await go(p.app, dagCbor.encode(p.start));
364
+ return { messages: n };
365
+ }
366
+ /** The kernel's `dispatch` operation's body: an add carries the whole row; a remove its key fields suffice (the row is sent as held). */
367
+ const rowBody = (r) => ({ op: r.op, row: r.row });
368
+ /** What uninstalling `name` sends: `stop`, then its routes removed (every route the table holds with `app: name`). The app's heads are left. */
369
+ export async function planUninstall(name, view) {
370
+ const a = await appRecordIn(view, name);
371
+ if (!a)
372
+ throw new Error(`${name}: no app installed under that head`);
373
+ return {
374
+ app: name, record: a.record, ...(a.record.stop ? { stop: a.record.stop.body } : {}),
375
+ rows: view.dispatch.filter((r) => r.app === name).map((r) => ({ op: "remove", row: r })),
376
+ };
377
+ }
378
+ export async function sendUninstall(p, send) {
379
+ let n = 0;
380
+ const go = async (box, body) => { await send(box, body); n++; };
381
+ if (p.stop)
382
+ await go(p.app, dagCbor.encode(p.stop));
383
+ for (const r of p.rows)
384
+ await go("dispatch", dagCbor.encode(rowBody(r)));
385
+ return { messages: n };
386
+ }
387
+ // ---------------------------------------------------------------- root's own routes (#143)
388
+ /**
389
+ * Root's own route (#143; `skein routes`): one `dispatch` add or remove of a route with no `app`
390
+ * — an app's upgrade or uninstall leaves it. The site at the instance's root is one: a read route
391
+ * {transport: "http", address: "/", prefix: true, filters: ["site.get"], root: "www"} (the site's
392
+ * filter answering every path no longer route takes). Refused at a key another route has; a filter
393
+ * of an app must be one its installed record declares.
394
+ */
395
+ export async function planRootRoute(view, op, row) {
396
+ const k = keyOf(row);
397
+ const held = view.dispatch.find((r) => keyOf(r) === k);
398
+ if (op === "add") {
399
+ if (held && held.app)
400
+ throw new Error(`route ${k}: app ${held.app} has it`);
401
+ for (const f of row.filters ?? []) {
402
+ const dot = f.lastIndexOf(".");
403
+ const app = f.slice(0, dot), name = f.slice(dot + 1);
404
+ if (app === "kernel")
405
+ continue;
406
+ const rec = await appRecordIn(view, app);
407
+ if (!rec?.record.filters || !(name in rec.record.filters))
408
+ throw new Error(`route ${k}: filter ${f}: no installed app ${app} declares it`);
409
+ }
410
+ }
411
+ else if (!held || held.app)
412
+ throw new Error(`route ${k}: root has no route there`);
413
+ const { app: _app, ...own } = row;
414
+ void _app;
415
+ // A remove sends the route as held (the kernel checks it whole).
416
+ const r = { op, row: op === "remove" ? held : own };
417
+ return { prompt: [`dispatch ${op} ${showRow(r)} (root's own: no app)`], rows: [r] };
418
+ }