@daugasauron/dolly-build 0.1.5-rc.0 → 0.1.6
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/README.md +259 -6
- package/host/requirements.mjs +1 -1
- package/host/runtime/abi.mjs +41 -0
- package/package.json +10 -4
- package/packages.json +101 -0
- package/{src → page}/image-entry.mjs +10 -6
- package/{src → page}/image-inputs.mjs +11 -0
- package/{src → page}/image-names.mjs +2 -2
- package/page/sha256.mjs +65 -0
- package/{src → page}/snapshot-records.mjs +63 -44
- package/{src → page}/static-asset.mjs +36 -20
- package/{src → page}/version.mjs +1 -1
- package/scripts/dolly-build-init/Dollyfile +3 -7
- package/scripts/dolly-build-init/public/icon.svg +1 -0
- package/scripts/dolly-build-init/public/index.html +6 -58
- package/scripts/dolly-build-init/public/main.mjs +52 -0
- package/scripts/dolly-build-init/public/page.css +3 -0
- package/scripts/dolly-build-init/public/skills/changelog/SKILL.md +5 -0
- package/scripts/dolly-build-init/workflow.yml +42 -0
- package/scripts/dolly-build.mjs +194 -62
- package/scripts/dollyfile-graph.mjs +3 -3
- package/scripts/image-directory.mjs +6 -7
- package/scripts/page-image-build.mjs +29 -10
- package/scripts/recipe-files.mjs +3 -3
- package/scripts/release-layout.mjs +1 -1
- package/scripts/serve.mjs +40 -10
- package/scripts/sha256.mjs +4 -0
- package/scripts/share-pages-snapshots.mjs +10 -30
- package/scripts/snapshot-identity.mjs +1 -2
- package/scripts/system-snapshot-format.mjs +11 -9
- package/scripts/wasm-interface.mjs +2 -2
- package/{src → site}/dollyfile-graph.mjs +1 -1
- package/{src → site}/dollyfile-view.mjs +1 -1
- /package/{src → page}/process-abi.mjs +0 -0
- /package/{src → page}/wasm-interface.mjs +0 -0
- /package/{src → site}/alphabetical.mjs +0 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { unretainedPath } from "./image-names.mjs";
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { DOLLY_SNAPSHOT_VERSION, DOLLY_SNAPSHOT_HEADER_BYTES as HEADER_BYTES, DOLLY_SNAPSHOT_RECORD_HEADER_BYTES as RECORD_HEADER_BYTES,
|
|
3
|
+
DOLLY_SNAPSHOT_RECORD_DIRECTORY as DIRECTORY, DOLLY_SNAPSHOT_RECORD_FILE as FILE, DOLLY_SNAPSHOT_RECORD_SYMLINK as SYMLINK,
|
|
4
|
+
DOLLY_SNAPSHOT_MAX_PATHS, DOLLY_SNAPSHOT_MAX_MEBIBYTES, DOLLY_SNAPSHOT_PATH_MIN_BYTES, DOLLY_SNAPSHOT_PATH_MAX_BYTES,
|
|
5
|
+
DOLLY_SNAPSHOT_SYMLINK_MIN_BYTES, DOLLY_SNAPSHOT_SYMLINK_MAX_BYTES } from "../host/runtime/abi.mjs";
|
|
6
|
+
// A snapshot part's layout and limits are abi/dolly-image-0.wat's.
|
|
7
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }), encoder = new TextEncoder();
|
|
8
|
+
export const MAX_SNAPSHOT_BYTES = DOLLY_SNAPSHOT_MAX_MEBIBYTES * 1024 * 1024;
|
|
4
9
|
// Packs average 2 MiB or more (scripts/share-pages-snapshots.mjs): an image of
|
|
5
10
|
// MAX_SNAPSHOT_BYTES needs about 1,024, which this bound leaves room for.
|
|
6
11
|
const MAX_SNAPSHOT_PACKS = MAX_SNAPSHOT_BYTES / (1024 * 1024);
|
|
@@ -12,27 +17,46 @@ function compareBytes(left, right) {
|
|
|
12
17
|
return left.length - right.length;
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
//
|
|
16
|
-
function
|
|
20
|
+
// A snapshot part's header: its record count.
|
|
21
|
+
export function decodeSnapshotHeader(bytes) {
|
|
17
22
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
18
|
-
if (bytes.length <
|
|
19
|
-
|
|
20
|
-
}
|
|
23
|
+
if (bytes.length < HEADER_BYTES || decoder.decode(bytes.subarray(0, 8)) !== "DOLLYSNP" ||
|
|
24
|
+
view.getUint32(8, true) !== DOLLY_SNAPSHOT_VERSION) throw new Error("invalid Dolly snapshot header");
|
|
21
25
|
const count = view.getUint32(12, true);
|
|
22
|
-
if (count === 0 || count >
|
|
26
|
+
if (count === 0 || count > DOLLY_SNAPSHOT_MAX_PATHS) throw new Error("invalid Dolly snapshot record count");
|
|
23
27
|
return count;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
|
-
// A record's
|
|
27
|
-
function decodeRecordHeader(bytes) {
|
|
30
|
+
// A record's header: its kind, path length and data size.
|
|
31
|
+
export function decodeRecordHeader(bytes) {
|
|
28
32
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
29
33
|
const kind = view.getUint32(0, true), pathLength = view.getUint32(4, true), size = view.getBigUint64(8, true);
|
|
30
|
-
if (kind <
|
|
31
|
-
size > BigInt(MAX_SNAPSHOT_BYTES) || (kind ===
|
|
32
|
-
(kind ===
|
|
34
|
+
if (kind < DIRECTORY || kind > SYMLINK || pathLength < DOLLY_SNAPSHOT_PATH_MIN_BYTES || pathLength > DOLLY_SNAPSHOT_PATH_MAX_BYTES ||
|
|
35
|
+
size > BigInt(MAX_SNAPSHOT_BYTES) || (kind === DIRECTORY && size !== 0n) ||
|
|
36
|
+
(kind === SYMLINK && (size < BigInt(DOLLY_SNAPSHOT_SYMLINK_MIN_BYTES) || size > BigInt(DOLLY_SNAPSHOT_SYMLINK_MAX_BYTES)))) {
|
|
37
|
+
throw new Error("invalid snapshot record size");
|
|
38
|
+
}
|
|
33
39
|
return { kind, pathLength, size: Number(size) };
|
|
34
40
|
}
|
|
35
41
|
|
|
42
|
+
export function snapshotHeader(count) {
|
|
43
|
+
const bytes = new Uint8Array(HEADER_BYTES), view = new DataView(bytes.buffer);
|
|
44
|
+
bytes.set(encoder.encode("DOLLYSNP"));
|
|
45
|
+
view.setUint32(8, DOLLY_SNAPSHOT_VERSION, true);
|
|
46
|
+
view.setUint32(12, count, true);
|
|
47
|
+
return bytes;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// A record's header and its encoded path PATH.
|
|
51
|
+
export function recordHeader(kind, path, size) {
|
|
52
|
+
const bytes = new Uint8Array(RECORD_HEADER_BYTES + path.length), view = new DataView(bytes.buffer);
|
|
53
|
+
view.setUint32(0, kind, true);
|
|
54
|
+
view.setUint32(4, path.length, true);
|
|
55
|
+
view.setBigUint64(8, BigInt(size), true);
|
|
56
|
+
bytes.set(path, RECORD_HEADER_BYTES);
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
// The path of PATHBYTES, valid and after PREVIOUS in byte order.
|
|
37
61
|
function decodePath(pathBytes, previous) {
|
|
38
62
|
const path = decoder.decode(pathBytes);
|
|
@@ -47,7 +71,7 @@ function checkParents(records) {
|
|
|
47
71
|
for (const path of records.keys()) {
|
|
48
72
|
for (let slash = path.indexOf("/", 1); slash !== -1; slash = path.indexOf("/", slash + 1)) {
|
|
49
73
|
const parent = records.get(path.slice(0, slash));
|
|
50
|
-
if (parent && parent.kind !==
|
|
74
|
+
if (parent && parent.kind !== DIRECTORY) throw new Error(`snapshot parent is not a directory: ${path}`);
|
|
51
75
|
}
|
|
52
76
|
}
|
|
53
77
|
}
|
|
@@ -55,20 +79,20 @@ function checkParents(records) {
|
|
|
55
79
|
export function decodeSnapshotRecords(input) {
|
|
56
80
|
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
57
81
|
if (bytes.length > MAX_SNAPSHOT_BYTES) throw new Error("invalid Dolly snapshot header");
|
|
58
|
-
const count =
|
|
82
|
+
const count = decodeSnapshotHeader(bytes.subarray(0, HEADER_BYTES));
|
|
59
83
|
const records = new Map();
|
|
60
|
-
let offset =
|
|
84
|
+
let offset = HEADER_BYTES, previous;
|
|
61
85
|
for (let index = 0; index < count; index += 1) {
|
|
62
|
-
if (offset > bytes.length -
|
|
63
|
-
const { kind, pathLength, size } = decodeRecordHeader(bytes.subarray(offset, offset +
|
|
64
|
-
offset +=
|
|
86
|
+
if (offset > bytes.length - RECORD_HEADER_BYTES) throw new Error("truncated snapshot record");
|
|
87
|
+
const { kind, pathLength, size } = decodeRecordHeader(bytes.subarray(offset, offset + RECORD_HEADER_BYTES));
|
|
88
|
+
offset += RECORD_HEADER_BYTES;
|
|
65
89
|
if (offset > bytes.length - pathLength - size) throw new Error("truncated snapshot record");
|
|
66
90
|
const pathBytes = bytes.subarray(offset, offset + pathLength);
|
|
67
91
|
const path = decodePath(pathBytes, previous);
|
|
68
92
|
previous = pathBytes;
|
|
69
93
|
offset += pathLength;
|
|
70
94
|
const data = bytes.subarray(offset, offset + size);
|
|
71
|
-
if (kind ===
|
|
95
|
+
if (kind === SYMLINK && data.includes(0)) throw new Error("invalid snapshot symlink");
|
|
72
96
|
records.set(path, { kind, data });
|
|
73
97
|
offset += size;
|
|
74
98
|
}
|
|
@@ -143,19 +167,19 @@ class PieceReader {
|
|
|
143
167
|
// snapshot's size.
|
|
144
168
|
export async function scanSnapshotRecords(pieces, keep) {
|
|
145
169
|
const reader = new PieceReader(pieces);
|
|
146
|
-
const count =
|
|
170
|
+
const count = decodeSnapshotHeader(await reader.take(HEADER_BYTES));
|
|
147
171
|
const records = new Map();
|
|
148
172
|
let previous;
|
|
149
173
|
for (let index = 0; index < count; index += 1) {
|
|
150
|
-
const { kind, pathLength, size } = decodeRecordHeader(await reader.take(
|
|
174
|
+
const { kind, pathLength, size } = decodeRecordHeader(await reader.take(RECORD_HEADER_BYTES));
|
|
151
175
|
const pathBytes = await reader.take(pathLength);
|
|
152
176
|
const path = decodePath(pathBytes, previous);
|
|
153
177
|
previous = pathBytes;
|
|
154
178
|
const offset = reader.position;
|
|
155
179
|
let data;
|
|
156
|
-
if (kind ===
|
|
180
|
+
if (kind === SYMLINK || (kind === FILE && keep(path))) {
|
|
157
181
|
data = await reader.take(size);
|
|
158
|
-
if (kind ===
|
|
182
|
+
if (kind === SYMLINK && data.includes(0)) throw new Error("invalid snapshot symlink");
|
|
159
183
|
} else await reader.skip(size);
|
|
160
184
|
records.set(path, { kind, data, offset, size });
|
|
161
185
|
}
|
|
@@ -165,35 +189,30 @@ export async function scanSnapshotRecords(pieces, keep) {
|
|
|
165
189
|
}
|
|
166
190
|
|
|
167
191
|
export function encodeSnapshotRecords(records) {
|
|
168
|
-
const encoder = new TextEncoder();
|
|
169
192
|
const entries = [...records].map(([path, record]) => ({ path: encoder.encode(path), ...record }))
|
|
170
193
|
.sort((left, right) => compareBytes(left.path, right.path));
|
|
171
|
-
const size = entries.reduce((sum, record) => sum +
|
|
172
|
-
if (size > MAX_SNAPSHOT_BYTES || entries.length === 0 || entries.length >
|
|
173
|
-
const bytes = new Uint8Array(size)
|
|
174
|
-
bytes.set(
|
|
175
|
-
|
|
176
|
-
view.setUint32(12, entries.length, true);
|
|
177
|
-
let offset = 16;
|
|
194
|
+
const size = entries.reduce((sum, record) => sum + RECORD_HEADER_BYTES + record.path.length + record.data.length, HEADER_BYTES);
|
|
195
|
+
if (size > MAX_SNAPSHOT_BYTES || entries.length === 0 || entries.length > DOLLY_SNAPSHOT_MAX_PATHS) throw new Error("snapshot exceeds record limits");
|
|
196
|
+
const bytes = new Uint8Array(size);
|
|
197
|
+
bytes.set(snapshotHeader(entries.length));
|
|
198
|
+
let offset = HEADER_BYTES;
|
|
178
199
|
for (const record of entries) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
bytes.set(record.data, offset + 16 + record.path.length);
|
|
184
|
-
offset += 16 + record.path.length + record.data.length;
|
|
200
|
+
const header = recordHeader(record.kind, record.path, record.data.length);
|
|
201
|
+
bytes.set(header, offset);
|
|
202
|
+
bytes.set(record.data, offset + header.length);
|
|
203
|
+
offset += header.length + record.data.length;
|
|
185
204
|
}
|
|
186
205
|
return bytes;
|
|
187
206
|
}
|
|
188
207
|
|
|
189
208
|
export function mergeSnapshotRecords(parts) {
|
|
190
209
|
const records = new Map();
|
|
191
|
-
let size =
|
|
210
|
+
let size = HEADER_BYTES;
|
|
192
211
|
for (const part of parts) {
|
|
193
212
|
for (const [path, record] of decodeSnapshotRecords(part)) {
|
|
194
213
|
if (records.has(path)) throw new Error(`duplicate packed snapshot path: ${path}`);
|
|
195
|
-
size +=
|
|
196
|
-
if (size > MAX_SNAPSHOT_BYTES || records.size >=
|
|
214
|
+
size += RECORD_HEADER_BYTES + encoder.encode(path).length + record.data.length;
|
|
215
|
+
if (size > MAX_SNAPSHOT_BYTES || records.size >= DOLLY_SNAPSHOT_MAX_PATHS) throw new Error("packed snapshot exceeds limits");
|
|
197
216
|
records.set(path, record);
|
|
198
217
|
}
|
|
199
218
|
}
|
|
@@ -205,15 +224,15 @@ export function mergeSnapshotRecords(parts) {
|
|
|
205
224
|
export function validateSnapshotPacks(metadata) {
|
|
206
225
|
if (!Array.isArray(metadata.packs) || metadata.packs.length === 0 || metadata.packs.length > MAX_SNAPSHOT_PACKS) throw new Error("invalid snapshot packs");
|
|
207
226
|
const hashes = new Set();
|
|
208
|
-
let size =
|
|
227
|
+
let size = HEADER_BYTES;
|
|
209
228
|
for (const pack of metadata.packs) {
|
|
210
229
|
if (!/^[0-9a-f]{64}$/.test(pack.sha256) || hashes.has(pack.sha256) ||
|
|
211
|
-
!Number.isSafeInteger(pack.byteLength) || pack.byteLength <
|
|
230
|
+
!Number.isSafeInteger(pack.byteLength) || pack.byteLength < HEADER_BYTES || pack.byteLength > MAX_SNAPSHOT_BYTES ||
|
|
212
231
|
!Number.isSafeInteger(pack.encodedByteLength) || pack.encodedByteLength <= 0 || pack.encodedByteLength > MAX_SNAPSHOT_BYTES) {
|
|
213
232
|
throw new Error("invalid snapshot pack descriptor");
|
|
214
233
|
}
|
|
215
234
|
hashes.add(pack.sha256);
|
|
216
|
-
size += pack.byteLength -
|
|
235
|
+
size += pack.byteLength - HEADER_BYTES;
|
|
217
236
|
if (size > MAX_SNAPSHOT_BYTES) throw new Error("packed snapshot exceeds size limit");
|
|
218
237
|
}
|
|
219
238
|
if (size !== metadata.byteLength) throw new Error("snapshot pack sizes do not match image");
|
|
@@ -61,31 +61,47 @@ export async function boundedAssetBody(response, limit, signal) {
|
|
|
61
61
|
// Pages before v0.1.5 read the X-Dolly-Parts header instead.
|
|
62
62
|
export const PARTS_MANIFEST_START = '{"byteLength":';
|
|
63
63
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
size += taken.length;
|
|
77
|
-
}
|
|
78
|
-
} finally {
|
|
79
|
-
// Not awaited: cancelling a copy settles only once the response is read too.
|
|
80
|
-
reader.cancel().catch(() => {});
|
|
64
|
+
// The first `length` bytes of a response's body, and a response whose body is
|
|
65
|
+
// all of it again, those bytes first. It reads the body itself: reading a
|
|
66
|
+
// copy of it and cancelling the copy cost the original its first bytes, in
|
|
67
|
+
// Firefox up to 151 and in Node, when other bodies were read meanwhile.
|
|
68
|
+
async function peeked(response, length) {
|
|
69
|
+
const reader = response.body.getReader(), chunks = [];
|
|
70
|
+
let size = 0, ended = false;
|
|
71
|
+
while (size < length) {
|
|
72
|
+
const next = await reader.read();
|
|
73
|
+
if (next.done) { ended = true; break; }
|
|
74
|
+
chunks.push(next.value);
|
|
75
|
+
size += next.value.length;
|
|
81
76
|
}
|
|
82
|
-
|
|
77
|
+
const head = new Uint8Array(Math.min(size, length));
|
|
78
|
+
for (let offset = 0, index = 0; offset < head.length; ++index) {
|
|
79
|
+
const taken = chunks[index].subarray(0, head.length - offset);
|
|
80
|
+
head.set(taken, offset);
|
|
81
|
+
offset += taken.length;
|
|
82
|
+
}
|
|
83
|
+
const body = new ReadableStream({
|
|
84
|
+
start(controller) {
|
|
85
|
+
for (const chunk of chunks) controller.enqueue(chunk);
|
|
86
|
+
if (ended) controller.close();
|
|
87
|
+
},
|
|
88
|
+
async pull(controller) {
|
|
89
|
+
const next = await reader.read();
|
|
90
|
+
if (next.done) controller.close(); else controller.enqueue(next.value);
|
|
91
|
+
},
|
|
92
|
+
cancel: reason => reader.cancel(reason),
|
|
93
|
+
}, { highWaterMark: 0 });
|
|
94
|
+
const whole = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
95
|
+
Object.defineProperty(whole, "url", { value: response.url });
|
|
96
|
+
return { head, whole };
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
// Streams verified parts, holding at most one 20 MiB part per request. Each
|
|
86
100
|
// part matches the manifest hash before any of its bytes are delivered.
|
|
87
|
-
export async function decodeStaticAsset(
|
|
88
|
-
if (!
|
|
101
|
+
export async function decodeStaticAsset(given, target, init, maximumBytes, fetchRequest = globalThis.fetch.bind(globalThis)) {
|
|
102
|
+
if (given.status !== 200 || !given.body) return given;
|
|
103
|
+
const start = new TextEncoder().encode(PARTS_MANIFEST_START), { head, whole: response } = await peeked(given, start.length);
|
|
104
|
+
if (head.length !== start.length || head.some((byte, index) => byte !== start[index])) return response;
|
|
89
105
|
const url = new URL(target);
|
|
90
106
|
if ((init.method ?? "GET") !== "GET" || url.search || url.hash ||
|
|
91
107
|
url.username || url.password || !/^https?:$/.test(url.protocol)) throw new Error("invalid multipart asset request");
|
package/{src → page}/version.mjs
RENAMED
|
@@ -1,18 +1,12 @@
|
|
|
1
1
|
# Pi reads its instructions, skills, extensions and themes from /home/dolly/.pi/agent/
|
|
2
2
|
# (Pi's docs/configuration.md, in the image under /usr/lib/node_modules/@earendil-works/pi-coding-agent/docs/).
|
|
3
3
|
# A FILE row writes its body, each line indented four spaces, into the image.
|
|
4
|
+
# The skills are the directories of public/skills/, which the page writes at boot.
|
|
4
5
|
|
|
5
6
|
FILE /home/dolly/.pi/agent/AGENTS.md
|
|
6
7
|
You are the coding agent of this project. Keep answers short, and run the
|
|
7
8
|
project's tests after a change.
|
|
8
9
|
|
|
9
|
-
FILE /home/dolly/.pi/agent/skills/changelog/SKILL.md
|
|
10
|
-
---
|
|
11
|
-
name: changelog
|
|
12
|
-
description: Keeps /workspace/CHANGELOG.md. Use when asked to record a change.
|
|
13
|
-
---
|
|
14
|
-
Add the change under "## Unreleased", one line each, newest first.
|
|
15
|
-
|
|
16
10
|
FILE /home/dolly/.pi/agent/extensions/hello.js
|
|
17
11
|
export default function (pi) {
|
|
18
12
|
pi.registerCommand("hello", {
|
|
@@ -52,3 +46,5 @@ FILE /etc/dolly/init.slop
|
|
|
52
46
|
# pi-key ANTHROPIC_API_KEY --offline --provider anthropic --model claude-sonnet-5-5
|
|
53
47
|
printf '\nPi exited; this is a Slop shell.\n'
|
|
54
48
|
/bin/foreground -i /bin/slop
|
|
49
|
+
|
|
50
|
+
ENTRY /bin/foreground -i /bin/slop /etc/dolly/init.slop
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><rect width="16" height="16" rx="3" fill="#f2d45c"/></svg>
|
|
@@ -1,61 +1,9 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<meta charset="utf-8">
|
|
3
3
|
<title>__NAME__</title>
|
|
4
|
-
<link rel="icon" href="
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
<main id="dolly" style="position:fixed;inset:0"></main>
|
|
11
|
-
<script type="module">
|
|
12
|
-
// Dolly needs cross-origin isolation. Where the host sends no COOP and COEP
|
|
13
|
-
// headers, coi-serviceworker.js adds them: the page registers it and loads again
|
|
14
|
-
// under it, once; boot() says so if that did not isolate it.
|
|
15
|
-
if (!crossOriginIsolated && navigator.serviceWorker && !sessionStorage.getItem("dolly-isolation-retry")) {
|
|
16
|
-
sessionStorage.setItem("dolly-isolation-retry", "1");
|
|
17
|
-
await navigator.serviceWorker.register("./coi-serviceworker.js");
|
|
18
|
-
await navigator.serviceWorker.ready;
|
|
19
|
-
location.reload();
|
|
20
|
-
await new Promise(() => {});
|
|
21
|
-
}
|
|
22
|
-
sessionStorage.removeItem("dolly-isolation-retry");
|
|
23
|
-
const { boot } = await import("./dolly/src/browser.mjs");
|
|
24
|
-
const { images, image, modules } = await import("./image/index.mjs");
|
|
25
|
-
|
|
26
|
-
// The hosts the agent may reach, and nothing else: the model's provider here,
|
|
27
|
-
// one rule per host the agent needs (api.github.com, pypi.org). A rule names an
|
|
28
|
-
// origin, its paths and methods, and the header that carries the key (docs/http.md
|
|
29
|
-
// in @daugasauron/dolly). Hosts without CORS headers go through a relay of your
|
|
30
|
-
// own (docs/agent-frontend.md).
|
|
31
|
-
const policy = { rules: [
|
|
32
|
-
{ origin: "https://openrouter.ai", pathPrefix: "/api/v1/", methods: ["GET", "POST"], credentialHeaders: ["authorization"] },
|
|
33
|
-
// { origin: "https://api.anthropic.com", pathPrefix: "/v1/", methods: ["POST"], credentialHeaders: ["x-api-key"] },
|
|
34
|
-
] };
|
|
35
|
-
|
|
36
|
-
// The project: workspace/ is written into /workspace at each boot. dolly-build
|
|
37
|
-
// lists its files in workspace.json beside this page. Pi's tools can read these
|
|
38
|
-
// files; they are not for secrets.
|
|
39
|
-
const listing = await fetch("./workspace.json");
|
|
40
|
-
if (!listing.ok) throw new Error("workspace.json must list the files under workspace/, as dolly-build writes it");
|
|
41
|
-
const files = {};
|
|
42
|
-
await Promise.all((await listing.json()).map(async path => {
|
|
43
|
-
const url = `./workspace/${path.split("/").map(encodeURIComponent).join("/")}`;
|
|
44
|
-
files[`/workspace/${path}`] = new Uint8Array(await (await fetch(url)).arrayBuffer());
|
|
45
|
-
}));
|
|
46
|
-
// The sandbox's colours, all in theme.json: Pi's theme (Pi's docs/themes.md), written
|
|
47
|
-
// over the image's, whose background, text, accent and muted the terminal and the
|
|
48
|
-
// popups take too (docs/embedding.md in @daugasauron/dolly).
|
|
49
|
-
const theme = await (await fetch("./theme.json")).text();
|
|
50
|
-
files["/home/dolly/.pi/agent/themes/dolly.json"] = theme;
|
|
51
|
-
const { vars } = JSON.parse(theme), look = document.querySelector("#dolly").style;
|
|
52
|
-
look.setProperty("--dolly-background", vars.background);
|
|
53
|
-
look.setProperty("--dolly-color", vars.text);
|
|
54
|
-
look.setProperty("--dolly-accent", vars.accent);
|
|
55
|
-
look.setProperty("--dolly-display-palette-8", vars.muted);
|
|
56
|
-
|
|
57
|
-
// Save (Ctrl+Shift+S) keeps the sandbox's state in this browser and names it in the
|
|
58
|
-
// address, ?session=NAME: the page then opens that save, as it was, in place of a fresh start.
|
|
59
|
-
const session = new URLSearchParams(location.search).get("session") ?? undefined;
|
|
60
|
-
await boot(document.querySelector("#dolly"), { images, image, modules, http: { policy }, files, session });
|
|
61
|
-
</script>
|
|
4
|
+
<link rel="icon" href="./icon.svg">
|
|
5
|
+
<link rel="stylesheet" href="./page.css">
|
|
6
|
+
<main id="dolly"></main>
|
|
7
|
+
<!-- No inline script or style: the site may serve the Content-Security-Policy docs/embedding.md
|
|
8
|
+
gives (in @daugasauron/dolly). -->
|
|
9
|
+
<script type="module" src="./main.mjs"></script>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Dolly needs cross-origin isolation. Where the host sends no COOP and COEP
|
|
2
|
+
// headers, coi-serviceworker.js adds them: the page registers it and loads again
|
|
3
|
+
// under it, once; boot() says so if that did not isolate it.
|
|
4
|
+
if (!crossOriginIsolated && navigator.serviceWorker && !sessionStorage.getItem("dolly-isolation-retry")) {
|
|
5
|
+
sessionStorage.setItem("dolly-isolation-retry", "1");
|
|
6
|
+
await navigator.serviceWorker.register("./coi-serviceworker.js");
|
|
7
|
+
await navigator.serviceWorker.ready;
|
|
8
|
+
location.reload();
|
|
9
|
+
await new Promise(() => {});
|
|
10
|
+
}
|
|
11
|
+
sessionStorage.removeItem("dolly-isolation-retry");
|
|
12
|
+
const { boot } = await import("./dolly/page/browser.mjs");
|
|
13
|
+
const { images, image, modules } = await import("./image/index.mjs");
|
|
14
|
+
|
|
15
|
+
// The hosts the agent may reach, and nothing else: the model's provider here,
|
|
16
|
+
// one rule per host the agent needs (api.github.com, pypi.org). A rule names an
|
|
17
|
+
// origin, its paths and methods, and the header that carries the key (docs/http.md
|
|
18
|
+
// in @daugasauron/dolly). Hosts without CORS headers go through a relay of your
|
|
19
|
+
// own (docs/agent-frontend.md).
|
|
20
|
+
const policy = { rules: [
|
|
21
|
+
{ origin: "https://openrouter.ai", pathPrefix: "/api/v1/", methods: ["GET", "POST"], credentialHeaders: ["authorization"] },
|
|
22
|
+
// { origin: "https://api.anthropic.com", pathPrefix: "/v1/", methods: ["POST"], credentialHeaders: ["x-api-key"] },
|
|
23
|
+
] };
|
|
24
|
+
|
|
25
|
+
// The project and Pi's skills: workspace/ is written into /workspace and each
|
|
26
|
+
// skills/NAME/ (a SKILL.md and the files it names, Pi's docs/skills.md) into Pi's
|
|
27
|
+
// skills at each boot. dolly-build lists their files in workspace.json and skills.json
|
|
28
|
+
// beside this page. Pi's tools can read these files; they are not for secrets.
|
|
29
|
+
const files = {};
|
|
30
|
+
for (const [tree, directory] of [["workspace", "/workspace"], ["skills", "/home/dolly/.pi/agent/skills"]]) {
|
|
31
|
+
const listing = await fetch(`./${tree}.json`);
|
|
32
|
+
if (!listing.ok) throw new Error(`${tree}.json must list the files under ${tree}/, as dolly-build writes it`);
|
|
33
|
+
await Promise.all((await listing.json()).map(async path => {
|
|
34
|
+
const url = `./${tree}/${path.split("/").map(encodeURIComponent).join("/")}`;
|
|
35
|
+
files[`${directory}/${path}`] = new Uint8Array(await (await fetch(url)).arrayBuffer());
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
// The sandbox's colours, all in theme.json: Pi's theme (Pi's docs/themes.md), written
|
|
39
|
+
// over the image's, whose background, text, accent and muted the terminal and the
|
|
40
|
+
// popups take too (docs/embedding.md in @daugasauron/dolly).
|
|
41
|
+
const theme = await (await fetch("./theme.json")).text();
|
|
42
|
+
files["/home/dolly/.pi/agent/themes/dolly.json"] = theme;
|
|
43
|
+
const { vars } = JSON.parse(theme), look = document.querySelector("#dolly").style;
|
|
44
|
+
look.setProperty("--dolly-background", vars.background);
|
|
45
|
+
look.setProperty("--dolly-color", vars.text);
|
|
46
|
+
look.setProperty("--dolly-accent", vars.accent);
|
|
47
|
+
look.setProperty("--dolly-display-palette-8", vars.muted);
|
|
48
|
+
|
|
49
|
+
// Save (Ctrl+Shift+S) keeps the sandbox's state in this browser and names it in the
|
|
50
|
+
// address, ?session=NAME: the page then opens that save, as it was, in place of a fresh start.
|
|
51
|
+
const session = new URLSearchParams(location.search).get("session") ?? undefined;
|
|
52
|
+
await boot(document.querySelector("#dolly"), { images, image, modules, http: { policy }, files, session });
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/* The page: Dolly fills it. The font of the popups of Pi's ask_user and show_html, and the
|
|
2
|
+
scheme of their form controls; their colours and the terminal's are theme.json's. */
|
|
3
|
+
#dolly { position: fixed; inset: 0; --dolly-html-font: ui-monospace, monospace; --dolly-html-color-scheme: dark; }
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Builds the site of Dollyfile-__NAME__ in Playwright's headless Chromium, the version
|
|
2
|
+
# package-lock.json pins. npm ci and that browser are its downloads: the runtime, the
|
|
3
|
+
# packages and the build are in node_modules. The site is the artifact github-pages.
|
|
4
|
+
name: __NAME__
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
pull_request:
|
|
10
|
+
workflow_dispatch:
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 22
|
|
20
|
+
cache: npm
|
|
21
|
+
- run: npm ci
|
|
22
|
+
- run: npx playwright-core install --with-deps chromium-headless-shell
|
|
23
|
+
- run: npx dolly-build Dollyfile-__NAME__ __NAME__ --browser chromium
|
|
24
|
+
- uses: actions/upload-pages-artifact@v3
|
|
25
|
+
with:
|
|
26
|
+
path: __NAME__
|
|
27
|
+
|
|
28
|
+
# Publishes the site on GitHub Pages from main, once the repository's Pages
|
|
29
|
+
# source is "GitHub Actions" (Settings, Pages): remove the "#" of these lines.
|
|
30
|
+
# deploy:
|
|
31
|
+
# if: github.ref == 'refs/heads/main'
|
|
32
|
+
# needs: build
|
|
33
|
+
# runs-on: ubuntu-latest
|
|
34
|
+
# permissions:
|
|
35
|
+
# pages: write
|
|
36
|
+
# id-token: write
|
|
37
|
+
# environment:
|
|
38
|
+
# name: github-pages
|
|
39
|
+
# url: ${{ steps.deployment.outputs.page_url }}
|
|
40
|
+
# steps:
|
|
41
|
+
# - id: deployment
|
|
42
|
+
# uses: actions/deploy-pages@v4
|