agentbox-app 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/LICENSE +674 -0
- package/README.md +176 -0
- package/bin/agentbox.mjs +103 -0
- package/briefs/adhd-mode.md +5 -0
- package/briefs/message-rules.md +95 -0
- package/briefs/worker.md +230 -0
- package/briefs/writing-rules.md +336 -0
- package/main/account-discovery.mjs +145 -0
- package/main/account-tooling.mjs +261 -0
- package/main/agent-activity.mjs +56 -0
- package/main/agent-files.mjs +422 -0
- package/main/agent-schedule.mjs +254 -0
- package/main/agent-sessions.mjs +435 -0
- package/main/agents.mjs +1095 -0
- package/main/analytics.mjs +206 -0
- package/main/answer-modes.mjs +206 -0
- package/main/approval-prompt-server.mjs +160 -0
- package/main/approvals.mjs +487 -0
- package/main/artifact-path.mjs +144 -0
- package/main/claude-bin.mjs +311 -0
- package/main/claude-input.mjs +117 -0
- package/main/claude-models.mjs +244 -0
- package/main/claude-plan.mjs +148 -0
- package/main/claude-usage.mjs +149 -0
- package/main/code-change.mjs +383 -0
- package/main/code-file.mjs +116 -0
- package/main/codex-account.mjs +146 -0
- package/main/codex-app-server.mjs +796 -0
- package/main/codex-approvals.mjs +712 -0
- package/main/codex-bin.mjs +261 -0
- package/main/codex-compaction.mjs +49 -0
- package/main/codex-models.mjs +194 -0
- package/main/codex-session.mjs +797 -0
- package/main/codex-threads.mjs +268 -0
- package/main/codex-usage-file.mjs +192 -0
- package/main/codex-usage.mjs +149 -0
- package/main/codex-watch.mjs +35 -0
- package/main/codex.mjs +757 -0
- package/main/config.mjs +345 -0
- package/main/copy-selection.mjs +55 -0
- package/main/crash-report.mjs +158 -0
- package/main/demo.mjs +254 -0
- package/main/dev-window.mjs +103 -0
- package/main/doc-file.mjs +99 -0
- package/main/doc-scheme.mjs +126 -0
- package/main/first-run.mjs +190 -0
- package/main/folders.mjs +91 -0
- package/main/fresh-user.mjs +120 -0
- package/main/git-change.mjs +323 -0
- package/main/img-scheme.mjs +90 -0
- package/main/instruction-settings.mjs +205 -0
- package/main/ipc.mjs +1558 -0
- package/main/live-replies.mjs +77 -0
- package/main/machine.mjs +113 -0
- package/main/main.mjs +972 -0
- package/main/menu.mjs +177 -0
- package/main/notify.mjs +155 -0
- package/main/open-source-file.mjs +8 -0
- package/main/project-identity.mjs +149 -0
- package/main/provider-commands.mjs +32 -0
- package/main/rail-note.mjs +74 -0
- package/main/repeats.mjs +261 -0
- package/main/row-label.mjs +185 -0
- package/main/screen-detail.mjs +44 -0
- package/main/serve.mjs +262 -0
- package/main/settings.mjs +912 -0
- package/main/staleness.mjs +76 -0
- package/main/store/chats.mjs +188 -0
- package/main/store/home.mjs +383 -0
- package/main/store/migrate.mjs +40 -0
- package/main/store/paths.mjs +82 -0
- package/main/store/pending-writes.mjs +122 -0
- package/main/store/project-lock.mjs +200 -0
- package/main/store/project.mjs +523 -0
- package/main/store/queues.mjs +31 -0
- package/main/store/work-items.mjs +299 -0
- package/main/store/write-queue.mjs +14 -0
- package/main/store-modules.mjs +23 -0
- package/main/store.mjs +1269 -0
- package/main/supervisor.mjs +6875 -0
- package/main/task-commands.mjs +161 -0
- package/main/task-folders-offthread.mjs +84 -0
- package/main/task-folders-thread.mjs +13 -0
- package/main/task-folders.mjs +409 -0
- package/main/task-remote-control.mjs +65 -0
- package/main/task-terminals.mjs +169 -0
- package/main/terminal-access.mjs +6 -0
- package/main/updater.mjs +240 -0
- package/main/zoom-keys.mjs +54 -0
- package/main/zoom-readout.mjs +20 -0
- package/mcp/agentbox-mcp.sh +34 -0
- package/mcp/core/account.mjs +77 -0
- package/mcp/core/chat.mjs +61 -0
- package/mcp/core/documents.mjs +238 -0
- package/mcp/core/page.mjs +152 -0
- package/mcp/core/products.mjs +110 -0
- package/mcp/core/skills.mjs +126 -0
- package/mcp/core/work.mjs +377 -0
- package/mcp/stdio.mjs +70 -0
- package/mcp/tools.mjs +257 -0
- package/package.json +186 -0
- package/renderer/dist/assets/Geist-Variable-Bj2R_7yk.woff2 +0 -0
- package/renderer/dist/assets/GeistMono-Variable-Dispecij.woff2 +0 -0
- package/renderer/dist/assets/addon-fit-YJmn1quW.js +12 -0
- package/renderer/dist/assets/addon-serialize-CsFQTvP_.js +13 -0
- package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
- package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
- package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
- package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
- package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
- package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
- package/renderer/dist/assets/gouache-valley-ChSu14-t.webp +0 -0
- package/renderer/dist/assets/gouache-valley-d9qasI6k.webp +0 -0
- package/renderer/dist/assets/gouache-valley-sm-CdLjT5Ec.webp +0 -0
- package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
- package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
- package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
- package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
- package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
- package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
- package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
- package/renderer/dist/assets/index-CwiDv5bL.css +32 -0
- package/renderer/dist/assets/index-D9hCAVPl.js +684 -0
- package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
- package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
- package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
- package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
- package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
- package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
- package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
- package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
- package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
- package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
- package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
- package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
- package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
- package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
- package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
- package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
- package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
- package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
- package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
- package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
- package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
- package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
- package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
- package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
- package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
- package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
- package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
- package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
- package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-ext-wght-normal-DzyfIafT.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-cyrillic-wght-normal-BMDVbyM7.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-ext-wght-normal-BWSLJLk6.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-greek-wght-normal-C9H9m1vD.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-ext-wght-normal-C8iNium2.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-latin-wght-normal-BqRLTx4X.woff2 +0 -0
- package/renderer/dist/assets/source-sans-3-vietnamese-wght-normal-C1uRvKPU.woff2 +0 -0
- package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
- package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
- package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
- package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
- package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
- package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
- package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
- package/renderer/dist/assets/xterm-BT7tBf8A.js +27 -0
- package/renderer/dist/emulator/audio-rules.mjs +74 -0
- package/renderer/dist/emulator/data/compression/README.md +11 -0
- package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
- package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
- package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
- package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
- package/renderer/dist/emulator/data/cores/README.md +22 -0
- package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
- package/renderer/dist/emulator/data/cores/package.json +65 -0
- package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
- package/renderer/dist/emulator/data/emulator.css +1614 -0
- package/renderer/dist/emulator/data/loader.js +170 -0
- package/renderer/dist/emulator/data/localization/README.md +63 -0
- package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
- package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
- package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
- package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
- package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
- package/renderer/dist/emulator/data/localization/en-US.json +339 -0
- package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
- package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
- package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
- package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
- package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
- package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
- package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
- package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
- package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
- package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
- package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
- package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
- package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
- package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
- package/renderer/dist/emulator/data/src/GameManager.js +464 -0
- package/renderer/dist/emulator/data/src/compression.js +142 -0
- package/renderer/dist/emulator/data/src/emulator.js +6158 -0
- package/renderer/dist/emulator/data/src/gamepad.js +167 -0
- package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
- package/renderer/dist/emulator/data/src/shaders.js +236 -0
- package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
- package/renderer/dist/emulator/data/src/storage.js +125 -0
- package/renderer/dist/emulator/data/version.json +5 -0
- package/renderer/dist/emulator/host.js +299 -0
- package/renderer/dist/emulator/index.html +28 -0
- package/renderer/dist/emulator/provenance.json +6 -0
- package/renderer/dist/fixtures/onboard/designs/w-n13/the-first-row.html +1 -0
- package/renderer/dist/index.html +13 -0
- package/scripts/after-install.mjs +59 -0
- package/scripts/approval-server.sh +43 -0
- package/scripts/before-publish.mjs +133 -0
- package/shared/agent-import.d.mts +13 -0
- package/shared/agent-import.mjs +252 -0
- package/shared/agents.d.mts +190 -0
- package/shared/agents.mjs +937 -0
- package/shared/analytics-events.mjs +63 -0
- package/shared/answers.d.mts +12 -0
- package/shared/answers.mjs +80 -0
- package/shared/artifact-keys.d.mts +28 -0
- package/shared/artifact-keys.mjs +140 -0
- package/shared/bridge-map.mjs +128 -0
- package/shared/claude-commands.d.mts +20 -0
- package/shared/claude-commands.generated.mjs +149 -0
- package/shared/claude-commands.mjs +131 -0
- package/shared/claude-models.generated.d.mts +15 -0
- package/shared/claude-models.generated.mjs +26 -0
- package/shared/claude-usage.d.mts +16 -0
- package/shared/claude-usage.mjs +172 -0
- package/shared/codex-commands.d.mts +4 -0
- package/shared/codex-commands.mjs +17 -0
- package/shared/codex-import.d.mts +38 -0
- package/shared/codex-import.mjs +232 -0
- package/shared/codex-modes.mjs +159 -0
- package/shared/codex-usage.mjs +180 -0
- package/shared/contracts.mjs +412 -0
- package/shared/crash-scrub.mjs +182 -0
- package/shared/dashboard.mjs +481 -0
- package/shared/dead-run-trace.mjs +127 -0
- package/shared/demo-world.mjs +415 -0
- package/shared/effort-levels.d.mts +15 -0
- package/shared/effort-levels.mjs +76 -0
- package/shared/engines.d.mts +62 -0
- package/shared/engines.mjs +389 -0
- package/shared/first-run-examples.mjs +60 -0
- package/shared/first-run-line.mjs +149 -0
- package/shared/first-run-practice.d.mts +32 -0
- package/shared/first-run-practice.mjs +254 -0
- package/shared/first-run-shape.mjs +100 -0
- package/shared/fresh-user-home.mjs +189 -0
- package/shared/hook-failure.mjs +22 -0
- package/shared/install-id.mjs +39 -0
- package/shared/instruction-defaults.json +5 -0
- package/shared/library-folder.mjs +28 -0
- package/shared/notify-rules.mjs +120 -0
- package/shared/product-name.d.mts +6 -0
- package/shared/product-name.mjs +144 -0
- package/shared/project-folder-check.d.mts +22 -0
- package/shared/project-folder-check.mjs +152 -0
- package/shared/provider-commands.d.mts +8 -0
- package/shared/provider-commands.mjs +58 -0
- package/shared/rank.d.mts +19 -0
- package/shared/rank.mjs +146 -0
- package/shared/recovery.mjs +26 -0
- package/shared/referenced-files.d.mts +11 -0
- package/shared/referenced-files.mjs +122 -0
- package/shared/repeats.d.mts +45 -0
- package/shared/repeats.mjs +193 -0
- package/shared/schemes.mjs +33 -0
- package/shared/settings-terminal.d.mts +1 -0
- package/shared/settings-terminal.mjs +14 -0
- package/shared/side-build.mjs +57 -0
- package/shared/spawn-trouble.d.mts +8 -0
- package/shared/spawn-trouble.mjs +599 -0
- package/shared/terminal-state.d.mts +1 -0
- package/shared/terminal-state.mjs +58 -0
- package/shared/usage.d.mts +53 -0
- package/shared/usage.mjs +319 -0
- package/shared/work-items.mjs +561 -0
- package/shared/work-lines.mjs +360 -0
- package/worker-permissions.json +6 -0
package/main/demo.mjs
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// A SECOND AGENTBOX WITH SOMEBODY ELSE'S WORK IN IT, FOR DEMOS.
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS IS THE NEW-USER ROW AND NOT A NEW MECHANISM. Being a new user
|
|
4
|
+
// already opens a second Agentbox inside a throwaway home
|
|
5
|
+
// (shared/fresh-user-home.mjs, main/fresh-user.mjs), and everything that makes
|
|
6
|
+
// that safe is exactly what a demo needs: her own copy keeps running with every
|
|
7
|
+
// agent in it, nothing of hers is read, and nothing anywhere is deleted. The
|
|
8
|
+
// single-instance lock is keyed on userData and a throwaway home is a different
|
|
9
|
+
// userData, so the two stand side by side. That property is the whole thing,
|
|
10
|
+
// and inventing a second way to get it would be inventing a second way to get
|
|
11
|
+
// it wrong.
|
|
12
|
+
//
|
|
13
|
+
// WHAT IS DIFFERENT IS WHAT IS IN THE STORE. A new user opens on an empty one,
|
|
14
|
+
// which is correct for testing a first run and useless for a demo: an inbox
|
|
15
|
+
// with nothing in it demonstrates nothing. So this seeds three invented
|
|
16
|
+
// products and a day of invented work first (shared/demo-world.mjs), and
|
|
17
|
+
// because the store then has projects in it, the welcome walk does not run
|
|
18
|
+
// (`firstRunNeeded` counts projects) and the copy opens straight on the inbox.
|
|
19
|
+
//
|
|
20
|
+
// AND HER ~/.claude IS NOT LINKED THROUGH, which is the one place this is
|
|
21
|
+
// deliberately stricter than the new-user row. That folder holds the Claude
|
|
22
|
+
// Code sessions already running on this Mac, and the app puts them in the
|
|
23
|
+
// inbox as rows with her real folder names on them. In a demo those names are
|
|
24
|
+
// on a projector. Claude Code itself and its login ARE linked, so answering a
|
|
25
|
+
// row in the demo starts a real agent on an invented product, which is the
|
|
26
|
+
// most convincing thing the demo can do.
|
|
27
|
+
//
|
|
28
|
+
// NOTHING IS DELETED HERE, EVER, and there is no version of this file that may
|
|
29
|
+
// delete anything outside `HOMES` below.
|
|
30
|
+
|
|
31
|
+
import fs from 'node:fs';
|
|
32
|
+
import os from 'node:os';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import { spawn } from 'node:child_process';
|
|
35
|
+
import { DEMO_DOCS, DEMO_ENV, DEMO_PRODUCTS, demoLedgerLines } from '../shared/demo-world.mjs';
|
|
36
|
+
import { prepareHome } from '../shared/fresh-user-home.mjs';
|
|
37
|
+
import { Name } from '../shared/product-name.mjs';
|
|
38
|
+
import { relaunchCommand } from './fresh-user.mjs';
|
|
39
|
+
import { encodeProjectPath, storeRootEnv } from './store/home.mjs';
|
|
40
|
+
import { NAME, nameSlug } from '../shared/product-name.mjs';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Where every demo home lives. Its own folder, NOT the first-run one: the two
|
|
44
|
+
* are cleared separately, and a demo home under the first-run folder would
|
|
45
|
+
* read as a fresh-user install to `runningAsAFreshUser` and send its counts
|
|
46
|
+
* under the wrong fixed id. */
|
|
47
|
+
export const HOMES = path.join(os.tmpdir(), `${nameSlug}-demo`);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The account the demo store is laid out under. It matches the id
|
|
51
|
+
* `main/config.mjs` defaults to, because the demo copy has no config file and
|
|
52
|
+
* will therefore look for its products at exactly this path. */
|
|
53
|
+
export const DEMO_ACCOUNT = '00000000-0000-4000-8000-000000000000';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Named by the clock, like a fresh-user home, so the newest sorts last and two
|
|
57
|
+
* demos opened a minute apart are told apart on disk. */
|
|
58
|
+
export function makeDemoHome(now = new Date()) {
|
|
59
|
+
const stamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
60
|
+
const home = path.join(HOMES, stamp);
|
|
61
|
+
fs.mkdirSync(home, { recursive: true });
|
|
62
|
+
return home;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The demo homes on disk, oldest first. */
|
|
66
|
+
export function demoHomes() {
|
|
67
|
+
if (!fs.existsSync(HOMES)) return [];
|
|
68
|
+
return fs
|
|
69
|
+
.readdirSync(HOMES)
|
|
70
|
+
.map((name) => path.join(HOMES, name))
|
|
71
|
+
.filter((dir) => { try { return fs.statSync(dir).isDirectory(); } catch { return false; } })
|
|
72
|
+
.sort();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Delete every demo home. Only ever this /tmp folder: `HOMES` is not
|
|
77
|
+
* configurable, and that is on purpose. */
|
|
78
|
+
export function clearDemoHomes() {
|
|
79
|
+
try {
|
|
80
|
+
fs.rmSync(HOMES, { recursive: true, force: true });
|
|
81
|
+
return { ok: true };
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return { ok: false, error: err?.message ?? String(err) };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Where the demo copy will keep its store, and where it will keep its own
|
|
89
|
+
* records. Both are derived from the throwaway home rather than configured,
|
|
90
|
+
* because the copy derives them the same way from the same home: `storeRoot`
|
|
91
|
+
* defaults to `<home>/<Name>` (main/config.mjs) and the app's home defaults
|
|
92
|
+
* to `<home>/.agentbox` (main/store/home.mjs).
|
|
93
|
+
*
|
|
94
|
+
* THE FOLDER IS READ FROM THE SAME CONSTANT AS THAT DEFAULT, not spelled out
|
|
95
|
+
* again. It was the literal 'the app' mirroring what config.mjs then said,
|
|
96
|
+
* and when that default was renamed on 2026-09-22 this line went on pointing
|
|
97
|
+
* at the old folder: the demo copy would have written its store to one place
|
|
98
|
+
* and looked for it in another. Two literals that have to agree is the defect;
|
|
99
|
+
* one import is the fix. */
|
|
100
|
+
export function demoPaths(home) {
|
|
101
|
+
const storeRoot = path.join(home, Name);
|
|
102
|
+
return {
|
|
103
|
+
storeRoot,
|
|
104
|
+
accountRoot: path.join(storeRoot, 'accounts', DEMO_ACCOUNT),
|
|
105
|
+
appHome: path.join(home, `.${nameSlug}`),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The invented studio, written out as a real store.
|
|
111
|
+
*
|
|
112
|
+
* THE LEDGER DOES NOT GO IN THE PRODUCT FOLDER, and this is the one thing in
|
|
113
|
+
* here that is easy to get wrong: since 2026-08-27 the app keeps its own
|
|
114
|
+
* records under its own home, `<appHome>/projects/<encoded product dir>/`
|
|
115
|
+
* (main/store/home.mjs). Writing `work-items.jsonl` beside `project.json`
|
|
116
|
+
* instead would produce a demo whose inbox is empty, and the file would then be
|
|
117
|
+
* silently folded away the first time anything touched the store. The encoding
|
|
118
|
+
* is imported rather than repeated for exactly that reason.
|
|
119
|
+
*
|
|
120
|
+
* `.origin` is written alongside, because that is what the app writes on first
|
|
121
|
+
* use and what `removeMachinery` refuses to act without.
|
|
122
|
+
*/
|
|
123
|
+
export function seedDemoStore(home, { now = Date.now() } = {}) {
|
|
124
|
+
const { accountRoot, appHome } = demoPaths(home);
|
|
125
|
+
const ledgers = demoLedgerLines(now);
|
|
126
|
+
const written = [];
|
|
127
|
+
|
|
128
|
+
for (const product of DEMO_PRODUCTS) {
|
|
129
|
+
const dir = path.join(accountRoot, product.slug);
|
|
130
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
131
|
+
|
|
132
|
+
fs.writeFileSync(path.join(dir, 'project.json'), `${JSON.stringify({
|
|
133
|
+
schemaVersion: 2,
|
|
134
|
+
id: product.slug,
|
|
135
|
+
name: product.name,
|
|
136
|
+
createdAt: new Date(now - 21 * 86_400_000).toISOString(),
|
|
137
|
+
oneLiner: product.oneLiner,
|
|
138
|
+
}, null, 2)}\n`);
|
|
139
|
+
|
|
140
|
+
// The documents, and the catalog that makes them visible. A file on disk
|
|
141
|
+
// with no entry in index.json is a file the pane does not know about
|
|
142
|
+
// (main/store/project.mjs), so both halves or neither.
|
|
143
|
+
const docs = DEMO_DOCS[product.slug] ?? [];
|
|
144
|
+
for (const doc of docs) {
|
|
145
|
+
const file = path.join(dir, doc.path);
|
|
146
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
147
|
+
fs.writeFileSync(file, `${doc.text}\n`);
|
|
148
|
+
}
|
|
149
|
+
fs.writeFileSync(path.join(dir, 'index.json'), `${JSON.stringify({
|
|
150
|
+
schemaVersion: 2,
|
|
151
|
+
creations: docs.map((doc, i) => ({
|
|
152
|
+
id: `${product.slug}-doc-${i + 1}`,
|
|
153
|
+
kind: 'markdown',
|
|
154
|
+
title: doc.title,
|
|
155
|
+
path: doc.path,
|
|
156
|
+
ts: now - (i + 1) * 3_600_000,
|
|
157
|
+
schemaVersion: 2,
|
|
158
|
+
})),
|
|
159
|
+
runs: [],
|
|
160
|
+
reports: [],
|
|
161
|
+
updatedAt: now,
|
|
162
|
+
}, null, 2)}\n`);
|
|
163
|
+
|
|
164
|
+
// And the work, where the app will actually look for it.
|
|
165
|
+
const machinery = path.join(appHome, 'projects', encodeProjectPath(dir));
|
|
166
|
+
fs.mkdirSync(machinery, { recursive: true });
|
|
167
|
+
fs.writeFileSync(path.join(machinery, '.origin'), `${dir}\n`);
|
|
168
|
+
const jsonl = (ledgers[product.slug] ?? []).map((line) => JSON.stringify(line)).join('\n');
|
|
169
|
+
fs.writeFileSync(path.join(machinery, 'work-items.jsonl'), jsonl ? `${jsonl}\n` : '');
|
|
170
|
+
|
|
171
|
+
// ROWS, NOT LINES, because this number is read out loud in the toast. One
|
|
172
|
+
// row is two or three appended lines (filed, then said, then finished), so
|
|
173
|
+
// counting the file's lines said twenty where she can see nine.
|
|
174
|
+
const ids = new Set((ledgers[product.slug] ?? []).map((line) => line.id));
|
|
175
|
+
written.push({ slug: product.slug, dir, machinery, rows: ids.size });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { accountRoot, appHome, products: written };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The environment a demo copy is launched with.
|
|
183
|
+
*
|
|
184
|
+
* BOTH HOME VARIABLES, ALWAYS, for the reason `freshEnv` gives: HOME alone
|
|
185
|
+
* does not move `app.getPath('appData')` on macOS, so the copy would share her
|
|
186
|
+
* userData, lose the single-instance lock race and quit in half a second.
|
|
187
|
+
*
|
|
188
|
+
* ASTRAL_HOME IS SET EXPLICITLY AND THAT IS NOT BELT AND BRACES. The app's home
|
|
189
|
+
* reads that variable first and only falls back to `~/.astral`, so a parent
|
|
190
|
+
* process that happens to have it set (every session Agentbox spawns does) would
|
|
191
|
+
* hand it straight through and the demo copy would read HER ledger while
|
|
192
|
+
* standing in an invented store. */
|
|
193
|
+
export function demoEnv(home, env = process.env) {
|
|
194
|
+
const { appHome } = demoPaths(home);
|
|
195
|
+
return { ...env, HOME: home, CFFIXED_USER_HOME: home, ...storeRootEnv(appHome), [DEMO_ENV]: '1' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Open one. Returns what happened in the words the app will show her, rather
|
|
200
|
+
* than throwing: a row in ⌘K that fails silently is worse than one that says
|
|
201
|
+
* why.
|
|
202
|
+
*
|
|
203
|
+
* `withAgents` is FALSE and has no parameter, unlike the new-user row. See the
|
|
204
|
+
* head of this file: linking her ~/.claude through would put her real folder
|
|
205
|
+
* names in the demo inbox, and the demo exists so that does not happen.
|
|
206
|
+
*/
|
|
207
|
+
export function openDemo({
|
|
208
|
+
execPath = process.execPath,
|
|
209
|
+
argv = process.argv,
|
|
210
|
+
packaged = true,
|
|
211
|
+
spawnFn = spawn,
|
|
212
|
+
now,
|
|
213
|
+
} = {}) {
|
|
214
|
+
let home;
|
|
215
|
+
let prepared;
|
|
216
|
+
let seeded;
|
|
217
|
+
try {
|
|
218
|
+
home = makeDemoHome(now ? new Date(now) : undefined);
|
|
219
|
+
prepared = prepareHome(home, { withAgents: false });
|
|
220
|
+
seeded = seedDemoStore(home, { now: now ?? Date.now() });
|
|
221
|
+
} catch (err) {
|
|
222
|
+
return { ok: false, error: `Could not lay out a demo under ${HOMES}: ${err?.message ?? err}` };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { bin, args } = relaunchCommand({ execPath, argv, packaged });
|
|
226
|
+
if (!bin || !fs.existsSync(bin)) {
|
|
227
|
+
return { ok: false, error: `Cannot find this app's own program at ${bin}.`, home };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
let child;
|
|
231
|
+
try {
|
|
232
|
+
child = spawnFn(bin, args, { detached: true, stdio: 'ignore', env: demoEnv(home) });
|
|
233
|
+
child.unref?.();
|
|
234
|
+
} catch (err) {
|
|
235
|
+
return { ok: false, error: `Could not start a second ${NAME}: ${err?.message ?? err}`, home };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// THE NOTES ARE THE HONEST PART, the same as the new-user row's. Both of
|
|
239
|
+
// these are things the demo copy can be missing while still looking fine, and
|
|
240
|
+
// both of them mean the same thing: answering a row in front of a room will
|
|
241
|
+
// not start an agent. Better said now than found out on the projector.
|
|
242
|
+
const notes = [];
|
|
243
|
+
if (!prepared.claudeBin) notes.push('Claude Code is not at ~/.local/bin/claude, so agents will not run in the demo.');
|
|
244
|
+
if (!prepared.keychain) notes.push('There is no ~/Library/Keychains here, so Claude Code will say it is not logged in.');
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
ok: true,
|
|
248
|
+
home,
|
|
249
|
+
products: seeded.products.map((p) => p.slug),
|
|
250
|
+
rows: seeded.products.reduce((n, p) => n + p.rows, 0),
|
|
251
|
+
notes,
|
|
252
|
+
pid: child?.pid ?? null,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// A HOT-RELOADING WINDOW MUST NOT BE THE ONE SHE WORKS IN.
|
|
2
|
+
//
|
|
3
|
+
// She was working in a window an agent had started against a vite dev server,
|
|
4
|
+
// on her real store. Vite rebuilds that window whenever the source under it
|
|
5
|
+
// changes, and the source under it is the checkout every agent shares: it was
|
|
6
|
+
// checked out or merged five times in the eleven minutes before she wrote.
|
|
7
|
+
//
|
|
8
|
+
// The one that gets her is a change to App.tsx that adds, removes or moves a
|
|
9
|
+
// hook, which is what a branch switch does to it. React Fast Refresh keeps a
|
|
10
|
+
// component's state only while its hooks match, so App is rebuilt from nothing:
|
|
11
|
+
// `snap` is null again, App.tsx draws `<div className="boot">the app</div>`, and
|
|
12
|
+
// every child of App is thrown away and remade. That is the screen she means by
|
|
13
|
+
// the zero loading page, and that is the artifact closing and reopening. The
|
|
14
|
+
// page never navigates, which is why nothing in the dev server's log looked bad
|
|
15
|
+
// enough to be it. Six triggers were measured against a real window and exactly
|
|
16
|
+
// one was guilty: scripts/what-throws-her-out-of-agentbox.mjs.
|
|
17
|
+
//
|
|
18
|
+
// THE GUARD IS HERE RATHER THAN IN scripts/dev.mjs BECAUSE THAT IS NOT HOW IT
|
|
19
|
+
// HAPPENED. The dev server that day was started by hand, straight out of a
|
|
20
|
+
// scratchpad, and the app was started beside it with the environment variable
|
|
21
|
+
// set. A guard in the npm script would have watched it go past. Every dev window
|
|
22
|
+
// there can ever be goes through main.mjs asking for this verdict, so this is
|
|
23
|
+
// the only place that actually closes the door.
|
|
24
|
+
//
|
|
25
|
+
// WHAT IS STILL ALLOWED, because a guard that stops the work is a guard somebody
|
|
26
|
+
// deletes:
|
|
27
|
+
//
|
|
28
|
+
// Fixtures. Nothing in a fixtures window is hers, so there is nothing to be
|
|
29
|
+
// thrown out of, and this is what screenshot and proof scripts use.
|
|
30
|
+
//
|
|
31
|
+
// A throwaway store. A script that points HOME at a temp directory has no
|
|
32
|
+
// accounts under its store root, and a window on an empty store is nobody's
|
|
33
|
+
// working day.
|
|
34
|
+
//
|
|
35
|
+
// Saying you meant it, with ZERO_DEV_ON_REAL_STORE=1.
|
|
36
|
+
|
|
37
|
+
// `storeHasWork` is passed in rather than read here so this stays a pure
|
|
38
|
+
// decision and can be tested without a disk.
|
|
39
|
+
export function hotWindowVerdict({ devUrl, fixtures, forced, storeRoot, storeHasWork }) {
|
|
40
|
+
if (!devUrl) return { allow: true, reason: 'no dev server' };
|
|
41
|
+
if (fixtures) return { allow: true, reason: 'fixtures' };
|
|
42
|
+
if (forced) return { allow: true, reason: 'ZERO_DEV_ON_REAL_STORE' };
|
|
43
|
+
if (!storeHasWork) return { allow: true, reason: 'no accounts under the store root' };
|
|
44
|
+
return {
|
|
45
|
+
allow: false,
|
|
46
|
+
reason: 'a hot-reloading window on the real store',
|
|
47
|
+
message: [
|
|
48
|
+
'',
|
|
49
|
+
` ZERO_DEV_URL is set and the store is ${storeRoot}, which is the real one.`,
|
|
50
|
+
' A window served by a dev server is rebuilt whenever any agent changes the',
|
|
51
|
+
' source under it, and being thrown back to the boot screen mid-task is what',
|
|
52
|
+
' w-435356146a was about. Not opening one.',
|
|
53
|
+
'',
|
|
54
|
+
' For development: ZERO_FIXTURES=1',
|
|
55
|
+
' To give her a real window: npm start, with ZERO_DEV_URL unset',
|
|
56
|
+
' If you truly meant this: ZERO_DEV_ON_REAL_STORE=1',
|
|
57
|
+
'',
|
|
58
|
+
].join('\n'),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Whether a store root has anybody's work under it. An account directory is the
|
|
63
|
+
// smallest true sign: a throwaway home has none, hers has one.
|
|
64
|
+
export function storeHasWork(fs, path, storeRoot) {
|
|
65
|
+
try {
|
|
66
|
+
const accounts = path.join(storeRoot, 'accounts');
|
|
67
|
+
return fs.readdirSync(accounts).some((name) => !name.startsWith('.'));
|
|
68
|
+
} catch {
|
|
69
|
+
// No store root, no accounts directory, no permission: all of them mean
|
|
70
|
+
// there is no working day here to interrupt.
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A DEV SERVER MUST NEVER INHERIT A WINDOW IT DID NOT OPEN.
|
|
76
|
+
//
|
|
77
|
+
// Second half of the same bug, found 2026-08-23 16:00 while getting her out of
|
|
78
|
+
// it. Killing the dev server under her window does not navigate it: vite's
|
|
79
|
+
// client logs "server connection lost, polling for restart" and then polls
|
|
80
|
+
// `${devUrl}` once a second forever, and it only calls `location.reload` once
|
|
81
|
+
// one of those pings SUCCEEDS
|
|
82
|
+
// (node_modules/vite/dist/client/client.mjs:560-562, waitForSuccessfulPing at
|
|
83
|
+
// :732). So her window survived intact with the server gone, but it was left
|
|
84
|
+
// waiting on port 5199 like a trap: the next `npm run dev` anybody ran would
|
|
85
|
+
// bind that port, her poll would succeed, and she would be thrown straight back
|
|
86
|
+
// into the boot screen and back onto the treadmill.
|
|
87
|
+
//
|
|
88
|
+
// A fixed port is what makes that possible, so dev servers stop using one. Each
|
|
89
|
+
// run takes a free ephemeral port and tells its own electron where to find it.
|
|
90
|
+
// A window polling yesterday's port then polls it forever and nothing answers,
|
|
91
|
+
// which is the whole point.
|
|
92
|
+
export function chooseDevPort(net) {
|
|
93
|
+
const server = net.createServer();
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
server.once('error', reject);
|
|
96
|
+
// Port 0 asks the OS for one nothing else holds, so this can never pick a
|
|
97
|
+
// port some other window is already waiting on.
|
|
98
|
+
server.listen(0, '127.0.0.1', () => {
|
|
99
|
+
const { port } = server.address();
|
|
100
|
+
server.close((err) => (err ? reject(err) : resolve(port)));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Reading and writing the markdown file the document pane has open.
|
|
2
|
+
//
|
|
3
|
+
// So the pane types straight onto the file on disk. Two rules make that safe
|
|
4
|
+
// enough to leave switched on:
|
|
5
|
+
//
|
|
6
|
+
// 1. ONLY MARKDOWN IS WRITABLE. An html page is a program; the pane shows it
|
|
7
|
+
// in a sandboxed frame and never edits it. Everything else the pane will
|
|
8
|
+
// not even open (renderer/src/doc-pane.ts).
|
|
9
|
+
// 2. A WRITE STAYS INSIDE THE PRODUCT'S OWN FOLDERS. Reading follows the
|
|
10
|
+
// finder's own rule and will honour an absolute path a worker typed, which
|
|
11
|
+
// is how a link into the app's repo works; writing will not. The file has
|
|
12
|
+
// to sit under the docs dir or the repo of the product whose card is open,
|
|
13
|
+
// or nothing is written.
|
|
14
|
+
//
|
|
15
|
+
// Pure of Electron on purpose: the IPC layer hands in the resolved path and the
|
|
16
|
+
// product, and these two functions are the whole of the disk access.
|
|
17
|
+
import fsDefault from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { artifactRoots } from './artifact-path.mjs';
|
|
20
|
+
|
|
21
|
+
export const EDITABLE_RE = /\.(md|markdown)$/i;
|
|
22
|
+
export const READABLE_RE = /\.(md|markdown|html?)$/i;
|
|
23
|
+
|
|
24
|
+
// A megabyte and a half of markdown is already far past anything in her store
|
|
25
|
+
// (the largest is decisions.md). Past that the editor is not the right tool and
|
|
26
|
+
// a silent truncation would eat her file on the next save, so it says so.
|
|
27
|
+
export const MAX_DOC_BYTES = 1_500_000;
|
|
28
|
+
|
|
29
|
+
/** Is `file` inside `dir`, without a prefix match like /a/bc counting as /a/b. */
|
|
30
|
+
export function isInside(dir, file) {
|
|
31
|
+
if (!dir || !file) return false;
|
|
32
|
+
const d = path.resolve(dir);
|
|
33
|
+
const f = path.resolve(file);
|
|
34
|
+
return f === d || f.startsWith(d + path.sep);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** May the pane write to this file, on this product. */
|
|
38
|
+
export function canWrite({ file, product, fs = fsDefault }) {
|
|
39
|
+
if (!file || !EDITABLE_RE.test(file)) {
|
|
40
|
+
return { ok: false, error: 'Only markdown files can be edited here.' };
|
|
41
|
+
}
|
|
42
|
+
const roots = artifactRoots(product);
|
|
43
|
+
if (!roots.some((root) => isInside(root, file))) {
|
|
44
|
+
const name = product?.name || 'this project';
|
|
45
|
+
return { ok: false, error: `That file is outside ${name}, so nothing was saved.` };
|
|
46
|
+
}
|
|
47
|
+
if (!fs.existsSync(file)) {
|
|
48
|
+
return { ok: false, error: 'That file is no longer there, so nothing was saved.' };
|
|
49
|
+
}
|
|
50
|
+
return { ok: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The text of a document the pane has open, with what it is and when it changed. */
|
|
54
|
+
export function readDoc({ file, fs = fsDefault }) {
|
|
55
|
+
if (!file || !READABLE_RE.test(file)) return { ok: false, error: 'That is not a document this pane can open.' };
|
|
56
|
+
let stat;
|
|
57
|
+
try { stat = fs.statSync(file); } catch { return { ok: false, error: 'That file is no longer there.' }; }
|
|
58
|
+
if (stat.size > MAX_DOC_BYTES) {
|
|
59
|
+
return { ok: false, error: 'That file is too big to open here. It opened outside instead.', tooBig: true };
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return { ok: true, path: file, text: fs.readFileSync(file, 'utf8'), mtime: stat.mtimeMs };
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return { ok: false, error: String(err?.message ?? err).slice(0, 200) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Her edit, onto the file.
|
|
70
|
+
*
|
|
71
|
+
* `mtime` is what the pane believed was on disk when it loaded. If the file has
|
|
72
|
+
* moved on since — an agent rewrote it while she was typing — the write is
|
|
73
|
+
* refused rather than silently winning, and the pane says so. The old editor
|
|
74
|
+
* holds an incoming agent update off while she has unsaved edits, which is the
|
|
75
|
+
* same rule from the other end; here the file is the shared thing, so the check
|
|
76
|
+
* is on the file.
|
|
77
|
+
*/
|
|
78
|
+
export function writeDoc({ file, product, text, mtime, fs = fsDefault }) {
|
|
79
|
+
const allowed = canWrite({ file, product, fs });
|
|
80
|
+
if (!allowed.ok) return allowed;
|
|
81
|
+
if (typeof text !== 'string') return { ok: false, error: 'Nothing to save.' };
|
|
82
|
+
if (mtime) {
|
|
83
|
+
let now = 0;
|
|
84
|
+
try { now = fs.statSync(file).mtimeMs; } catch { /* handled by canWrite */ }
|
|
85
|
+
// A second's slack: some filesystems round mtime, and our own write is what
|
|
86
|
+
// moved it last in the ordinary case.
|
|
87
|
+
if (now && now - mtime > 1000) {
|
|
88
|
+
return { ok: false, stale: true, error: 'Something else changed this file while you were typing, so nothing was saved.' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
fs.writeFileSync(file, text, 'utf8');
|
|
93
|
+
let after = 0;
|
|
94
|
+
try { after = fs.statSync(file).mtimeMs; } catch { /* the write succeeded */ }
|
|
95
|
+
return { ok: true, mtime: after };
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return { ok: false, error: String(err?.message ?? err).slice(0, 200) };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// THE ORIGIN THE DOCUMENT PANE SERVES A PAGE FROM, so its pictures load.
|
|
2
|
+
//
|
|
3
|
+
// She was reading one of our own design pages and every <img> in it was a
|
|
4
|
+
// broken placeholder with the alt text beside it.
|
|
5
|
+
//
|
|
6
|
+
// WHY THEY WERE BROKEN, measured before this was written. The pane drew the
|
|
7
|
+
// page in `<iframe sandbox="allow-scripts" src="file://…">`. A sandbox without
|
|
8
|
+
// allow-same-origin gives the frame an OPAQUE origin, and Chromium refuses to
|
|
9
|
+
// let an opaque origin pull a file:// subresource. The probe: the same image,
|
|
10
|
+
// same folder, same frame.
|
|
11
|
+
//
|
|
12
|
+
// sandbox="allow-scripts" naturalWidth 0
|
|
13
|
+
// console: "Not allowed to load local resource: file:///…/shot.png"
|
|
14
|
+
// sandbox="allow-scripts allow-same-origin" naturalWidth 2880
|
|
15
|
+
//
|
|
16
|
+
// So the one-line fix is to add allow-same-origin, AND THAT IS THE HOLE, not
|
|
17
|
+
// the fix. The app's own window is loaded with loadFile (main.mjs), so the app
|
|
18
|
+
// itself is a file:// page. A file:// frame that is allowed to be same-origin
|
|
19
|
+
// sits in the same origin as the app, and a page an agent wrote could then
|
|
20
|
+
// reach straight into her inbox. Chromium says as much out loud: "An iframe
|
|
21
|
+
// which has both allow-scripts and allow-same-origin for its sandbox attribute
|
|
22
|
+
// can escape its sandboxing."
|
|
23
|
+
//
|
|
24
|
+
// So the page gets an origin of its OWN instead. This scheme is registered as
|
|
25
|
+
// standard and secure, the pane points the frame at astral-doc://file/…, and
|
|
26
|
+
// the frame may keep allow-same-origin because "same origin" now means "the
|
|
27
|
+
// same as other documents", not "the same as Agentbox". Measured in the same
|
|
28
|
+
// probe, on this scheme:
|
|
29
|
+
//
|
|
30
|
+
// naturalWidth 2880, origin "astral-doc://file", parent.document -> BLOCKED
|
|
31
|
+
//
|
|
32
|
+
// WHAT MAY BE SERVED. A page can ask this scheme for any path it likes, so the
|
|
33
|
+
// answer cannot be "whatever is on disk". Grants below are the whole of it: the
|
|
34
|
+
// pane names a file when it opens one, and only that file's own folder and the
|
|
35
|
+
// product's own artifact roots become readable. Her real design pages do climb
|
|
36
|
+
// out of their own folder — 17 references across the 200 html files in this
|
|
37
|
+
// product point at ../attachments, ../landing and ../../landing/assets — which
|
|
38
|
+
// is why the roots are granted and not just the folder.
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
import { DOC_SCHEME, DOC_SCHEMES, isScheme } from '../shared/schemes.mjs';
|
|
41
|
+
|
|
42
|
+
// The name is read rather than typed, and every spelling this app has used is
|
|
43
|
+
// still answered, because a url on this scheme is STORED. shared/schemes.mjs
|
|
44
|
+
// carries that argument in full.
|
|
45
|
+
export { DOC_SCHEME, DOC_SCHEMES };
|
|
46
|
+
|
|
47
|
+
// A standard scheme needs a host, and this one has exactly one: every document
|
|
48
|
+
// is a full absolute path under it. Keeping a single host keeps a single
|
|
49
|
+
// origin, so a page and the picture beside it never look cross-origin to each
|
|
50
|
+
// other.
|
|
51
|
+
export const DOC_HOST = 'file';
|
|
52
|
+
|
|
53
|
+
/** The url the pane points its frame at, for a file already resolved on disk. */
|
|
54
|
+
export function docUrl(file) {
|
|
55
|
+
if (typeof file !== 'string' || !path.isAbsolute(file)) return null;
|
|
56
|
+
// Each segment on its own: encodeURIComponent would eat the separators, and
|
|
57
|
+
// encodeURI would leave '#' and '?' in a filename to be read as url syntax.
|
|
58
|
+
const parts = file.split('/').filter(Boolean).map(encodeURIComponent);
|
|
59
|
+
return `${DOC_SCHEME}://${DOC_HOST}/${parts.join('/')}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The file a request under this scheme is asking for, or null if it is not one. */
|
|
63
|
+
export function docPath(url) {
|
|
64
|
+
let parsed;
|
|
65
|
+
try { parsed = new URL(String(url)); } catch { return null; }
|
|
66
|
+
if (!isScheme(DOC_SCHEMES, parsed.protocol)) return null;
|
|
67
|
+
let file;
|
|
68
|
+
try { file = decodeURIComponent(parsed.pathname); } catch { return null; }
|
|
69
|
+
if (!file.startsWith('/')) return null;
|
|
70
|
+
// The url is the only thing between a page and the disk, so a climb written
|
|
71
|
+
// into it is normalised away here rather than trusted to the grant check.
|
|
72
|
+
const normal = path.normalize(file);
|
|
73
|
+
return path.isAbsolute(normal) ? normal : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Is `file` inside `dir`, without /a/bc counting as inside /a/b. */
|
|
77
|
+
function inside(dir, file) {
|
|
78
|
+
if (!dir || !file) return false;
|
|
79
|
+
const d = path.resolve(dir);
|
|
80
|
+
const f = path.resolve(file);
|
|
81
|
+
return f === d || f.startsWith(d + path.sep);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* What the scheme is allowed to hand out.
|
|
86
|
+
*
|
|
87
|
+
* The pane opens one document at a time, so this stays small on purpose: a
|
|
88
|
+
* grant is made when a document is resolved for the pane and the oldest falls
|
|
89
|
+
* off. It is a plain object with no Electron in it so the rule can be tested
|
|
90
|
+
* for real rather than asserted against the source.
|
|
91
|
+
*/
|
|
92
|
+
export class DocGrants {
|
|
93
|
+
constructor(limit = 8) {
|
|
94
|
+
this.limit = limit;
|
|
95
|
+
this.roots = [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Open this file, and the folders that come with it, to the scheme. */
|
|
99
|
+
grant(file, roots = []) {
|
|
100
|
+
if (typeof file !== 'string' || !path.isAbsolute(file)) return;
|
|
101
|
+
const add = [path.dirname(file), ...roots.filter((r) => typeof r === 'string' && r)];
|
|
102
|
+
for (const root of add) {
|
|
103
|
+
const r = path.resolve(root);
|
|
104
|
+
if (this.roots.includes(r)) continue;
|
|
105
|
+
this.roots.push(r);
|
|
106
|
+
}
|
|
107
|
+
while (this.roots.length > this.limit) this.roots.shift();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** May the scheme read this path. */
|
|
111
|
+
allows(file) {
|
|
112
|
+
if (typeof file !== 'string' || !path.isAbsolute(file)) return false;
|
|
113
|
+
return this.roots.some((root) => inside(root, file));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A PAGE GOES OUT EXACTLY AS IT WAS WRITTEN, and that is deliberate.
|
|
118
|
+
//
|
|
119
|
+
// For a few hours on 2026-08-21 this file also injected a listener into every
|
|
120
|
+
// page it served, so a pinch with the cursor inside the document could reach
|
|
121
|
+
// the app and zoom the page on its own.
|
|
122
|
+
//
|
|
123
|
+
// So the document has no zoom of its own. The app zoom (main/menu.mjs,
|
|
124
|
+
// main/zoom-keys.mjs) scales the window, and the pane is an ordinary iframe in
|
|
125
|
+
// that window, so an open file follows it like everything else. Do not put a
|
|
126
|
+
// listener back into a served page: nothing is injected into a file she reads.
|