samagotchi 0.149.0 → 0.151.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +69 -1
- data/README.md +4 -2
- data/docs/architecture.md +4 -4
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +59 -24
- data/docs/configuration.md +6 -6
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/block-renderers.md +96 -0
- data/docs/internals/bridge.md +53 -9
- data/docs/internals/bundles.md +43 -14
- data/docs/internals/objectives.md +7 -2
- data/docs/internals/web-frontend.md +2 -2
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +48 -10
- data/docs/plugins.md +49 -3
- data/docs/sessions.md +5 -4
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/block_renderers.rb +115 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_places.rb +93 -0
- data/lib/samagotchi/bridge/card_store.rb +224 -82
- data/lib/samagotchi/bridge.rb +12 -4
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/engine.rb +19 -1
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +23 -3
- data/lib/samagotchi/memory_bundle/builder.rb +5 -3
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +70 -6
- data/lib/samagotchi/memory_bundle/installed_copy.rb +1 -0
- data/lib/samagotchi/memory_bundle/installer.rb +162 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/manifest.rb +23 -2
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +182 -22
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/objectives_check.rb +37 -7
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/session_chain.rb +2 -7
- data/lib/samagotchi/session_commands.rb +15 -9
- data/lib/samagotchi/session_manager.rb +10 -7
- data/lib/samagotchi/steer.rb +17 -0
- data/lib/samagotchi/terminal_ui.rb +4 -4
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_checkpoint.rb +11 -5
- data/lib/samagotchi/turn_flow.rb +2 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +22 -12
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
- data/lib/samagotchi/web/markdown_renderer.rb +39 -6
- data/lib/samagotchi/web/message_display.rb +3 -3
- data/lib/samagotchi/web/public/app.js +7 -3
- data/lib/samagotchi/web/public/block_frame.js +202 -0
- data/lib/samagotchi/web/public/block_host.js +502 -0
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/block_renderers.css +20 -0
- data/lib/samagotchi/web/public/block_source.js +37 -0
- data/lib/samagotchi/web/public/bubbles.js +11 -5
- data/lib/samagotchi/web/public/card.js +17 -10
- data/lib/samagotchi/web/public/cards_place.js +10 -5
- data/lib/samagotchi/web/public/claim_link.js +4 -2
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/history_reveal.js +4 -1
- data/lib/samagotchi/web/public/index.html +20 -14
- data/lib/samagotchi/web/public/markdown_place.js +2 -3
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/text_roots.js +6 -0
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_html.js +4 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_show.rb +1 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- data/lib/samagotchi/worker.rb +4 -2
- metadata +17 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// One language's renderer in a hidden sandboxed frame: a bundle's JS, whose
|
|
2
|
+
// input is model-written, must not reach a page that controls sessions. The
|
|
3
|
+
// layers and rules are docs/internals/block-renderers.md.
|
|
4
|
+
//
|
|
5
|
+
// `sandbox="allow-scripts"`, never allow-same-origin. The frame has no network
|
|
6
|
+
// and no cookie, so its code comes by postMessage from the page's own
|
|
7
|
+
// authenticated fetch and enters as `script.text` (eval doesn't work: a library's
|
|
8
|
+
// "use strict" keeps its global local; a syntax error doesn't throw, it reaches
|
|
9
|
+
// the frame's error event). The protocol, the handshake first:
|
|
10
|
+
//
|
|
11
|
+
// frame → loaded, page → code {files}, frame → ready | failed {message}
|
|
12
|
+
// page → render {id, source, theme}, frame → done {id, svg} | error {id, message}
|
|
13
|
+
//
|
|
14
|
+
// One render at a time. A timer can't fire while the renderer hangs (the frame
|
|
15
|
+
// shares the page's thread): block_host.js's mark is for that.
|
|
16
|
+
//
|
|
17
|
+
// A module of its own: static JS is cached at unversioned URLs.
|
|
18
|
+
|
|
19
|
+
// Same words as the spike that tested them; the page's frame-src 'none' covers
|
|
20
|
+
// what this doesn't (navigation).
|
|
21
|
+
export const FRAME_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; font-src data:; form-action 'none'";
|
|
22
|
+
export const FRAME_TIMEOUT_MS = 15000;
|
|
23
|
+
// An SVG this size isn't a diagram anyone reads, and it would sit in the DOM.
|
|
24
|
+
export const MAX_SVG_CHARS = 8 * 1024 * 1024;
|
|
25
|
+
|
|
26
|
+
// Runs in the frame, before any bundle code. CSP doesn't stop WebRTC, so its
|
|
27
|
+
// constructors go (a nested srcdoc frame gets a fresh one: the residual risk,
|
|
28
|
+
// docs/internals/block-renderers.md).
|
|
29
|
+
const BOOTSTRAP = `(() => {
|
|
30
|
+
delete window.RTCPeerConnection;
|
|
31
|
+
delete window.webkitRTCPeerConnection;
|
|
32
|
+
const send = (m) => parent.postMessage(m, "*");
|
|
33
|
+
const words = (e) => String((e && e.message) || e).split("\\n")[0].slice(0, 300);
|
|
34
|
+
let renderer = null;
|
|
35
|
+
let loadError = null;
|
|
36
|
+
window.chi = { defineRenderer(r) { renderer = r; } };
|
|
37
|
+
addEventListener("error", (e) => { loadError = loadError || words(e.message || e.error); });
|
|
38
|
+
async function load(files) {
|
|
39
|
+
for (const code of files) {
|
|
40
|
+
const tag = document.createElement("script");
|
|
41
|
+
tag.text = code;
|
|
42
|
+
document.head.append(tag);
|
|
43
|
+
tag.remove();
|
|
44
|
+
if (loadError) return send({ type: "failed", message: loadError });
|
|
45
|
+
}
|
|
46
|
+
if (!renderer || typeof renderer.render !== "function") return send({ type: "failed", message: "no chi.defineRenderer" });
|
|
47
|
+
try { await (renderer.init && renderer.init()); } catch (e) { return send({ type: "failed", message: words(e) }); }
|
|
48
|
+
send({ type: "ready" });
|
|
49
|
+
}
|
|
50
|
+
async function render(m) {
|
|
51
|
+
try {
|
|
52
|
+
const svg = await renderer.render(m.source, m.theme);
|
|
53
|
+
if (typeof svg !== "string") throw new Error("the renderer returned no SVG");
|
|
54
|
+
send({ type: "done", id: m.id, svg });
|
|
55
|
+
} catch (e) {
|
|
56
|
+
send({ type: "error", id: m.id, message: words(e) });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
addEventListener("message", (e) => {
|
|
60
|
+
if (e.source !== parent) return;
|
|
61
|
+
if (e.data.type === "code") load(e.data.files);
|
|
62
|
+
else if (e.data.type === "render") render(e.data);
|
|
63
|
+
});
|
|
64
|
+
send({ type: "loaded" });
|
|
65
|
+
})();`;
|
|
66
|
+
|
|
67
|
+
export function frameSrcdoc() {
|
|
68
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="${FRAME_CSP}"><script>${BOOTSTRAP}<\/script></head><body></body></html>`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const TYPES = new Set(["loaded", "ready", "failed", "done", "error"]);
|
|
72
|
+
|
|
73
|
+
// What +event+ says, or null: only +frameWindow+'s, of a known type, and read
|
|
74
|
+
// as data from here on (rule 3). No other window `message` listener exists on
|
|
75
|
+
// this page, so this is the only receiver.
|
|
76
|
+
export function frameMessage(event, frameWindow) {
|
|
77
|
+
if (!frameWindow || event.source !== frameWindow) return null;
|
|
78
|
+
const data = event.data;
|
|
79
|
+
if (!data || typeof data !== "object" || !TYPES.has(data.type)) return null;
|
|
80
|
+
return data;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A reason a render or load failed. +timedOut+: the page's timer fired, so the
|
|
84
|
+
// block is suspect (block_host.js keeps its mark); +dropped+: the page let the
|
|
85
|
+
// frame go, which says nothing of the block.
|
|
86
|
+
export class FrameError extends Error {
|
|
87
|
+
constructor(message, { timedOut = false, dropped = false, load = false } = {}) {
|
|
88
|
+
super(message);
|
|
89
|
+
this.timedOut = timedOut;
|
|
90
|
+
this.dropped = dropped;
|
|
91
|
+
this.load = load;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// @param files the renderer's file names, in load order
|
|
96
|
+
// @param fetchFile (file) => a promise of its text (block_host.js fetchRendererFile)
|
|
97
|
+
// @return {render(source, theme, {onStart}) → Promise<svg>, drop(), dead()}
|
|
98
|
+
export function createBlockFrame({ files, fetchFile, doc = document, win = window, timeoutMs = FRAME_TIMEOUT_MS }) {
|
|
99
|
+
const iframe = doc.createElement("iframe");
|
|
100
|
+
iframe.setAttribute("sandbox", "allow-scripts");
|
|
101
|
+
iframe.setAttribute("aria-hidden", "true");
|
|
102
|
+
iframe.setAttribute("tabindex", "-1");
|
|
103
|
+
// Off-screen, not display:none: a renderer's layout measures text in it.
|
|
104
|
+
iframe.style.cssText = "position:fixed;left:-10000px;top:0;width:1000px;height:800px;border:0";
|
|
105
|
+
|
|
106
|
+
let dead = false;
|
|
107
|
+
let nextId = 0;
|
|
108
|
+
// What the frame is being waited for: {type?, id?, resolve, reject, timer}.
|
|
109
|
+
let waiting = null;
|
|
110
|
+
|
|
111
|
+
function die(error) {
|
|
112
|
+
if (dead) return;
|
|
113
|
+
dead = true;
|
|
114
|
+
win.removeEventListener("message", onMessage);
|
|
115
|
+
iframe.remove();
|
|
116
|
+
settle((w) => w.reject(error));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function settle(answer) {
|
|
120
|
+
const w = waiting;
|
|
121
|
+
waiting = null;
|
|
122
|
+
if (!w) return;
|
|
123
|
+
clearTimeout(w.timer);
|
|
124
|
+
answer(w);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The frame's next answer, bounded.
|
|
128
|
+
function answer(what, loadStage = false) {
|
|
129
|
+
if (dead) return Promise.reject(new FrameError("the frame is gone", { dropped: true }));
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
const timer = setTimeout(() => die(new FrameError(`timed out (${what})`, { timedOut: !loadStage, load: loadStage })), timeoutMs);
|
|
132
|
+
waiting = { what, resolve, reject, timer, loadStage };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let loadedSeen = null;
|
|
137
|
+
function onMessage(event) {
|
|
138
|
+
const m = frameMessage(event, iframe.contentWindow);
|
|
139
|
+
if (!m) return;
|
|
140
|
+
if (m.type === "loaded") { loadedSeen?.(); return; }
|
|
141
|
+
if (!waiting) return;
|
|
142
|
+
if (m.type === "ready" && waiting.what === "ready") settle((w) => w.resolve());
|
|
143
|
+
else if (m.type === "failed" && waiting.what === "ready") {
|
|
144
|
+
settle((w) => w.reject(new FrameError(String(m.message ?? "load failed"), { load: true })));
|
|
145
|
+
} else if (m.type === "done" && m.id === waiting.id) {
|
|
146
|
+
if (typeof m.svg !== "string" || m.svg.length > MAX_SVG_CHARS) settle((w) => w.reject(new FrameError("the renderer's SVG is too large or not text")));
|
|
147
|
+
else settle((w) => w.resolve(m.svg));
|
|
148
|
+
} else if (m.type === "error" && m.id === waiting.id) {
|
|
149
|
+
settle((w) => w.reject(new FrameError(String(m.message ?? "render failed"))));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const loaded = new Promise((resolve) => { loadedSeen = resolve; });
|
|
154
|
+
win.addEventListener("message", onMessage);
|
|
155
|
+
iframe.srcdoc = frameSrcdoc();
|
|
156
|
+
doc.body.append(iframe);
|
|
157
|
+
|
|
158
|
+
async function load() {
|
|
159
|
+
const texts = await Promise.all(files.map((file) => fetchFile(file)));
|
|
160
|
+
const handshake = answer("load", true);
|
|
161
|
+
loaded.then(() => {
|
|
162
|
+
// Stop waiting on the handshake's own timer and start the code's.
|
|
163
|
+
if (dead) return;
|
|
164
|
+
settle((w) => w.resolve());
|
|
165
|
+
});
|
|
166
|
+
await handshake;
|
|
167
|
+
const ready = answer("ready", true);
|
|
168
|
+
iframe.contentWindow.postMessage({ type: "code", files: texts }, "*");
|
|
169
|
+
await ready;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ready = load().catch((e) => {
|
|
173
|
+
const error = e instanceof FrameError ? e : new FrameError(String(e?.message || e), { load: true });
|
|
174
|
+
die(error);
|
|
175
|
+
throw error;
|
|
176
|
+
});
|
|
177
|
+
// One render at a time, whatever the last one came to.
|
|
178
|
+
let queue = ready.catch(() => {});
|
|
179
|
+
|
|
180
|
+
function render(source, theme, { onStart = () => {} } = {}) {
|
|
181
|
+
const run = async () => {
|
|
182
|
+
await ready;
|
|
183
|
+
if (dead) throw new FrameError("the frame is gone", { dropped: true });
|
|
184
|
+
onStart();
|
|
185
|
+
const id = ++nextId;
|
|
186
|
+
const done = answer(`render ${id}`);
|
|
187
|
+
waiting.id = id;
|
|
188
|
+
iframe.contentWindow.postMessage({ type: "render", id, source, theme }, "*");
|
|
189
|
+
return done;
|
|
190
|
+
};
|
|
191
|
+
const result = queue.then(run);
|
|
192
|
+
queue = result.catch(() => {});
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
ready,
|
|
198
|
+
render,
|
|
199
|
+
dead: () => dead,
|
|
200
|
+
drop: () => die(new FrameError("dropped", { dropped: true })),
|
|
201
|
+
};
|
|
202
|
+
}
|