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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { SCHEMA_VERSION } from '../../shared/contracts.mjs';
|
|
2
|
+
|
|
3
|
+
// One-time index.json v1 -> v2: artifacts[] becomes creations[]. Pure and
|
|
4
|
+
// idempotent; readIndex decides whether to write the result back (it writes
|
|
5
|
+
// only when the returned object differs from what was read).
|
|
6
|
+
export function migrateIndex(idx) {
|
|
7
|
+
const src = idx ?? {};
|
|
8
|
+
const shaped = (src.schemaVersion === SCHEMA_VERSION && Array.isArray(src.creations))
|
|
9
|
+
? src
|
|
10
|
+
: (() => {
|
|
11
|
+
const { artifacts, ...rest } = src;
|
|
12
|
+
return {
|
|
13
|
+
...rest,
|
|
14
|
+
schemaVersion: SCHEMA_VERSION,
|
|
15
|
+
creations: Array.isArray(src.creations) ? src.creations : (artifacts ?? []),
|
|
16
|
+
runs: src.runs ?? [], reports: src.reports ?? [],
|
|
17
|
+
updatedAt: src.updatedAt ?? null,
|
|
18
|
+
};
|
|
19
|
+
})();
|
|
20
|
+
// Lazy cleanups ride the read path:
|
|
21
|
+
// - Database/deploy summary docs written before 2026-07-11 carried a phantom
|
|
22
|
+
// kind 'doc' that was never in CREATION_KINDS; they are markdown.
|
|
23
|
+
// - One creation represents the whole application (her call, 2026-07-11):
|
|
24
|
+
// legacy standalone database viewers (kind 'database') and the sibling
|
|
25
|
+
// database/deployment receipt docs tombstone away. Their files stay on
|
|
26
|
+
// disk untouched; the live views are the app creation's Data/People
|
|
27
|
+
// panes, and receipts now land INSIDE the app creation as DATABASE.md /
|
|
28
|
+
// DEPLOYMENT.md the next time provisioning or deploy runs.
|
|
29
|
+
const phantom = (c) => c?.kind === 'doc';
|
|
30
|
+
const standalone = (c) => !c?.deleted && (c?.kind === 'database' || c?.meta?.databaseFor != null || c?.meta?.deploymentFor != null);
|
|
31
|
+
if (!shaped.creations.some((c) => phantom(c) || standalone(c))) return shaped;
|
|
32
|
+
return {
|
|
33
|
+
...shaped,
|
|
34
|
+
creations: shaped.creations.map((c) => {
|
|
35
|
+
let next = phantom(c) ? { ...c, kind: 'markdown' } : c;
|
|
36
|
+
if (standalone(next)) next = { ...next, deleted: true };
|
|
37
|
+
return next;
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// The .git / .env deny-list, shared by EVERY creation-file access point: the
|
|
5
|
+
// agent tools (main/agent/tools.mjs), the folder-backed materialize walk
|
|
6
|
+
// (main/creations/materialize.mjs, which re-exports these), the creation-file://
|
|
7
|
+
// protocol (main/index.mjs), and the single-file store SINK
|
|
8
|
+
// (readCreationContent/saveCreation in main/store/project.mjs). It lives HERE,
|
|
9
|
+
// the lowest-level path module both store/ and creations/ already import, so
|
|
10
|
+
// there is exactly ONE copy of the rules and no import cycle (store must not
|
|
11
|
+
// import up into creations/). A trailing-segment match on a DIRECTORY named
|
|
12
|
+
// e.g. .env.local blocks everything nested under it, not just a file literally
|
|
13
|
+
// named that.
|
|
14
|
+
export const GIT_PATH_RE = /(^|\/)\.git(\/|$)/;
|
|
15
|
+
export const ENV_PATH_RE = /(^|\/)\.env[^/]*(\/|$)/;
|
|
16
|
+
|
|
17
|
+
// Deny checks run on a normalized form: backslashes folded to forward slashes
|
|
18
|
+
// (assertInside treats both as separators) and lowercased, so a case-
|
|
19
|
+
// insensitive filesystem (the default on macOS/Windows) can never let
|
|
20
|
+
// `.Git/config` alias the real `.git` past a case-sensitive regex.
|
|
21
|
+
export function normalizeForDenyCheck(rel) {
|
|
22
|
+
return String(rel).replace(/\\/g, '/').toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// True iff `rel` names a .git or .env path in ANY segment. null/undefined -> false.
|
|
26
|
+
// The single canonical predicate; other modules re-export it rather than
|
|
27
|
+
// duplicating the regexes.
|
|
28
|
+
export function deniesGitOrEnvPath(rel) {
|
|
29
|
+
if (rel == null) return false;
|
|
30
|
+
const norm = normalizeForDenyCheck(rel);
|
|
31
|
+
return GIT_PATH_RE.test(norm) || ENV_PATH_RE.test(norm);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Containment: resolve rel against root; realpath the nearest EXISTING
|
|
35
|
+
// ancestor so a symlinked directory cannot smuggle the path outside; reject
|
|
36
|
+
// a symlink as the final component (lstat). Throws on any violation, returns
|
|
37
|
+
// the absolute (real-ancestor-based) path on success.
|
|
38
|
+
export function assertInside(root, rel) {
|
|
39
|
+
if (typeof rel !== 'string' || !rel) throw new Error('empty path');
|
|
40
|
+
if (path.isAbsolute(rel)) throw new Error(`absolute path rejected: ${rel}`);
|
|
41
|
+
if (rel.split(/[\\/]/).some((seg) => seg === '..')) throw new Error(`dot-dot rejected: ${rel}`);
|
|
42
|
+
const realRoot = fs.realpathSync(root);
|
|
43
|
+
const target = path.resolve(realRoot, rel);
|
|
44
|
+
// nearest existing ancestor, realpathed, must stay inside
|
|
45
|
+
let probe = target;
|
|
46
|
+
while (!fs.existsSync(probe)) probe = path.dirname(probe);
|
|
47
|
+
const realProbe = fs.realpathSync(probe);
|
|
48
|
+
if (realProbe !== realRoot && !realProbe.startsWith(realRoot + path.sep)) {
|
|
49
|
+
throw new Error(`path escapes project: ${rel}`);
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
if (fs.lstatSync(target).isSymbolicLink()) throw new Error(`symlink rejected: ${rel}`);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
if (e.code !== 'ENOENT') throw e;
|
|
55
|
+
}
|
|
56
|
+
return target;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Same check as a boolean, for canUseTool-style callers.
|
|
60
|
+
export function isInside(root, rel) {
|
|
61
|
+
try { assertInside(root, rel); return true; } catch { return false; }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A folder-backed creation's `dir` must be a real, non-empty subdirectory of
|
|
65
|
+
// creations/ (creations/<slug>), NEVER the creations root itself
|
|
66
|
+
// (dir: "creations", "creations/", "creations/.", ...) or some other project
|
|
67
|
+
// subtree (a run directory, the project root, ...). plain assertInside alone
|
|
68
|
+
// is not enough here: it only proves `dir` does not escape the PROJECT, not
|
|
69
|
+
// that it is a legitimate creation root, and every consumer that trusts
|
|
70
|
+
// envelope.dir as a creation root (read/write/list/delete a file inside it)
|
|
71
|
+
// must reject a bad one itself, not rely on validation having already run
|
|
72
|
+
// once when the envelope was first cataloged. A stale or externally-supplied
|
|
73
|
+
// envelope (e.g. merged in from a sync peer, or written before this check
|
|
74
|
+
// existed) must never grant access outside its own creation folder.
|
|
75
|
+
export function assertCreationDir(root, dir) {
|
|
76
|
+
const rel = String(dir ?? '').replace(/\\/g, '/');
|
|
77
|
+
const segments = rel.split('/').filter(Boolean);
|
|
78
|
+
if (segments[0] !== 'creations' || segments.length < 2 || segments[1] === '.' || segments[1] === '..') {
|
|
79
|
+
throw new Error(`unsafe creation dir (must be a real subdirectory of creations/): ${dir}`);
|
|
80
|
+
}
|
|
81
|
+
return assertInside(root, rel);
|
|
82
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// A write the ledger refused, kept on disk until somebody delivers it.
|
|
2
|
+
//
|
|
3
|
+
// The claim fence is right and stays: a worker whose lease has lapsed must not
|
|
4
|
+
// stamp over whoever picked the row up next. What was wrong is what happened
|
|
5
|
+
// after the refusal, which was nothing. The write was thrown away, the session
|
|
6
|
+
// usually ended on that call, and neither the founder nor the agent ever learned
|
|
7
|
+
// a message had existed. Measured across 3,191 worker transcripts on this
|
|
8
|
+
// machine between 2 and 24 August 2026: 233 writes refused for a lapsed claim,
|
|
9
|
+
// 50 of them never landed on the row by any route, carrying 101,606 characters
|
|
10
|
+
// of finished notes and results. Half were the session's closing message.
|
|
11
|
+
//
|
|
12
|
+
// So a refused write is parked here instead, and the next session to claim that
|
|
13
|
+
// row is handed it. Parking is deliberately NOT delivery: writing it onto the
|
|
14
|
+
// row behind the fence would be the overwrite the fence exists to stop. The
|
|
15
|
+
// message waits for someone who legitimately holds the claim to decide what of
|
|
16
|
+
// it is still true.
|
|
17
|
+
//
|
|
18
|
+
// One file per refused write, in the app's own home, because a home survives a
|
|
19
|
+
// reboot and /tmp does not. It used to be the product's own folder, which
|
|
20
|
+
// survives one just as well and is also the worker's working directory
|
|
21
|
+
// (main/store/home.mjs).
|
|
22
|
+
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { machineryPath } from './home.mjs';
|
|
26
|
+
import { isWorkItemId } from '../../shared/work-items.mjs';
|
|
27
|
+
|
|
28
|
+
const DIR = 'pending-writes';
|
|
29
|
+
// Delivered messages move here rather than being deleted: the next session
|
|
30
|
+
// after that one should not be handed a message already carried, and nothing
|
|
31
|
+
// about this should ever be the reason something becomes unrecoverable.
|
|
32
|
+
const COLLECTED = 'collected';
|
|
33
|
+
|
|
34
|
+
// A holder id reaches us from an MCP client and ends up in a filename.
|
|
35
|
+
function safeHolder(holder) {
|
|
36
|
+
return String(holder ?? 'unknown').replace(/[^A-Za-z0-9._-]/g, '-').slice(0, 40) || 'unknown';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// In the app's home beside the ledger, not in the product folder. The reason
|
|
40
|
+
// the header gives still holds — this must survive a reboot and /tmp does not —
|
|
41
|
+
// and `~/.astral` survives one exactly as well (main/store/home.mjs).
|
|
42
|
+
function itemDir(projectDir, id) {
|
|
43
|
+
if (!isWorkItemId(id)) throw new Error(`invalid work item id: ${id}`);
|
|
44
|
+
return machineryPath(projectDir, path.join(DIR, id));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Keep a refused write. Returns where it went, so the caller can say so in the
|
|
49
|
+
* error it raises: an agent that is told its message survived and where can
|
|
50
|
+
* re-claim and re-send, which is the whole difference between a lost night and
|
|
51
|
+
* a retry.
|
|
52
|
+
*/
|
|
53
|
+
export function parkPendingWrite(projectDir, id, patch, { holder, epoch = null, reason = '', now = Date.now() } = {}) {
|
|
54
|
+
const dir = itemDir(projectDir, id);
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
const name = `${now}-${safeHolder(holder)}.json`;
|
|
57
|
+
const file = path.join(dir, name);
|
|
58
|
+
const record = {
|
|
59
|
+
item: id,
|
|
60
|
+
ts: now,
|
|
61
|
+
writtenAt: new Date(now).toISOString(),
|
|
62
|
+
holder: holder ?? null,
|
|
63
|
+
epoch,
|
|
64
|
+
refusedBecause: reason,
|
|
65
|
+
patch: patch ?? {},
|
|
66
|
+
};
|
|
67
|
+
fs.writeFileSync(file, `${JSON.stringify(record, null, 1)}\n`, 'utf8');
|
|
68
|
+
return { ...record, file, relPath: path.join(DIR, id, name) };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Every write still waiting on this row, oldest first. */
|
|
72
|
+
export function readPendingWrites(projectDir, id) {
|
|
73
|
+
let dir;
|
|
74
|
+
try { dir = itemDir(projectDir, id); } catch { return []; }
|
|
75
|
+
let names = [];
|
|
76
|
+
try {
|
|
77
|
+
names = fs.readdirSync(dir, { withFileTypes: true })
|
|
78
|
+
.filter((e) => e.isFile() && e.name.endsWith('.json'))
|
|
79
|
+
.map((e) => e.name);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
if (err.code === 'ENOENT') return [];
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
const out = [];
|
|
85
|
+
for (const name of names.sort()) {
|
|
86
|
+
let record = null;
|
|
87
|
+
try { record = JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); } catch { continue; }
|
|
88
|
+
if (!record || typeof record !== 'object') continue;
|
|
89
|
+
out.push({ ...record, file: path.join(dir, name), relPath: path.join(DIR, id, name) });
|
|
90
|
+
}
|
|
91
|
+
return out.sort((a, b) => (a.ts ?? 0) - (b.ts ?? 0));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Mark everything waiting on this row as delivered, by the session that was
|
|
96
|
+
* handed it. Called when that session actually writes to the row: it has been
|
|
97
|
+
* given the message and has now spoken, so a third session must not be handed
|
|
98
|
+
* the same words again. The files move rather than vanish.
|
|
99
|
+
*/
|
|
100
|
+
export function collectPendingWrites(projectDir, id, { by = null, now = Date.now() } = {}) {
|
|
101
|
+
const waiting = readPendingWrites(projectDir, id);
|
|
102
|
+
if (!waiting.length) return [];
|
|
103
|
+
const dir = itemDir(projectDir, id);
|
|
104
|
+
const done = path.join(dir, COLLECTED);
|
|
105
|
+
fs.mkdirSync(done, { recursive: true });
|
|
106
|
+
const moved = [];
|
|
107
|
+
for (const record of waiting) {
|
|
108
|
+
const target = path.join(done, path.basename(record.file));
|
|
109
|
+
try {
|
|
110
|
+
fs.writeFileSync(record.file, `${JSON.stringify({ ...stripPaths(record), collectedBy: by, collectedAt: new Date(now).toISOString() }, null, 1)}\n`, 'utf8');
|
|
111
|
+
fs.renameSync(record.file, target);
|
|
112
|
+
moved.push(target);
|
|
113
|
+
} catch { /* a message we cannot move is left where it is, which is safe */ }
|
|
114
|
+
}
|
|
115
|
+
return moved;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function stripPaths({ file, relPath, ...rest }) {
|
|
119
|
+
return rest;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const _internals = { DIR, COLLECTED, itemDir };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// A cross-process advisory lock, one per project.
|
|
2
|
+
//
|
|
3
|
+
// Why this exists: main/store/write-queue.mjs serializes catalog writes inside
|
|
4
|
+
// ONE process, which was the whole world when the only writer was the app's
|
|
5
|
+
// main process. It is not the world any more. Stateless wake-ups mean a dozen
|
|
6
|
+
// MCP server children can be live at once, each against the same store, and
|
|
7
|
+
// an in-process promise chain says nothing about any of them. readIndex ->
|
|
8
|
+
// mutate -> atomicWriteJson across two processes is a lost update: both read
|
|
9
|
+
// revision N, both write, one document silently vanishes from the catalog.
|
|
10
|
+
// atomicWriteJson is atomic per write (tmp + rename); it is not a
|
|
11
|
+
// compare-and-swap, and nothing in the store ever pretended otherwise.
|
|
12
|
+
//
|
|
13
|
+
// The mechanism is the oldest one there is: create a file with O_EXCL, which
|
|
14
|
+
// the kernel guarantees will succeed for exactly one caller.
|
|
15
|
+
//
|
|
16
|
+
// THE HARD PART IS NOT TAKING THE LOCK, IT IS BREAKING A STALE ONE. A holder
|
|
17
|
+
// can die without releasing (crash, SIGKILL, a laptop lid), so a lock nobody
|
|
18
|
+
// holds must eventually be breakable or the store wedges silently, which is the
|
|
19
|
+
// expensive failure. But the obvious test, "is the holder's pid alive", is a
|
|
20
|
+
// HINT AND NOT PROOF: pids get recycled, so an unrelated process inheriting a
|
|
21
|
+
// dead holder's number would keep a lock alive forever, and the wedge is
|
|
22
|
+
// permanent and invisible. So:
|
|
23
|
+
//
|
|
24
|
+
// - a random token, not the pid, is what identifies a holder
|
|
25
|
+
// - a conservative timeout is the real arbiter of staleness
|
|
26
|
+
// - the pid check is used only in the direction it is sound: a pid that is
|
|
27
|
+
// GONE proves the holder is gone (that lock can be broken early). A pid
|
|
28
|
+
// that is alive proves nothing and buys no extra time.
|
|
29
|
+
// - critical sections stay tiny (read, mutate, rename, release) so the window
|
|
30
|
+
// in which a wrongly-broken lock could matter is measured in milliseconds
|
|
31
|
+
|
|
32
|
+
import fs from 'node:fs';
|
|
33
|
+
import path from 'node:path';
|
|
34
|
+
import crypto from 'node:crypto';
|
|
35
|
+
|
|
36
|
+
// How long a lock may be held before another process may break it. Far longer
|
|
37
|
+
// than any legitimate critical section here (all of which are a read, a JSON
|
|
38
|
+
// mutate, and a rename) and far shorter than a founder would tolerate a wedge.
|
|
39
|
+
export const STALE_MS = 30_000;
|
|
40
|
+
// Total time to wait for a contended lock before giving up. A dozen writers
|
|
41
|
+
// queueing on sub-millisecond sections never comes close to this.
|
|
42
|
+
const ACQUIRE_TIMEOUT_MS = 15_000;
|
|
43
|
+
// Backoff between attempts: fast at first (the common case is microseconds of
|
|
44
|
+
// contention), then easing off so a genuinely stuck lock is not spun on.
|
|
45
|
+
const BACKOFF_MS = [1, 2, 5, 10, 20, 50, 100, 200];
|
|
46
|
+
|
|
47
|
+
const lockPath = (projectDir) => path.join(projectDir, '.lock');
|
|
48
|
+
|
|
49
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
50
|
+
|
|
51
|
+
// Is this pid gone? Signal 0 checks existence without delivering anything.
|
|
52
|
+
// Only the FALSE answer here is trustworthy (see the header): a live pid may be
|
|
53
|
+
// a recycled number, but a missing one is definitely not our holder.
|
|
54
|
+
function pidGone(pid) {
|
|
55
|
+
if (!Number.isInteger(pid) || pid <= 0) return true;
|
|
56
|
+
try { process.kill(pid, 0); return false; } catch (err) { return err.code === 'ESRCH'; }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readHolder(file) {
|
|
60
|
+
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Try once. Returns the token on success, null if someone else holds it.
|
|
64
|
+
function tryAcquire(file, label) {
|
|
65
|
+
const token = crypto.randomBytes(12).toString('hex');
|
|
66
|
+
try {
|
|
67
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
68
|
+
const fd = fs.openSync(file, 'wx'); // O_CREAT | O_EXCL: exactly one winner
|
|
69
|
+
try {
|
|
70
|
+
fs.writeSync(fd, JSON.stringify({ token, pid: process.pid, at: Date.now(), label }));
|
|
71
|
+
} finally {
|
|
72
|
+
fs.closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
return token;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err.code !== 'EEXIST') throw err;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Break a lock that is provably or presumptively abandoned. The token check is
|
|
82
|
+
// what makes this safe against the race with a legitimate holder releasing at
|
|
83
|
+
// the same moment: we only unlink the exact holder we decided was stale, so a
|
|
84
|
+
// fresh lock taken in between survives.
|
|
85
|
+
function breakIfStale(file, now = Date.now()) {
|
|
86
|
+
const held = readHolder(file);
|
|
87
|
+
if (!held) {
|
|
88
|
+
// Unreadable or truncated: a torn write from a holder that died mid-create.
|
|
89
|
+
// Age it out rather than trusting a file we cannot parse.
|
|
90
|
+
try {
|
|
91
|
+
const age = now - fs.statSync(file).mtimeMs;
|
|
92
|
+
if (age > STALE_MS) fs.unlinkSync(file);
|
|
93
|
+
} catch { /* already gone; someone else won the break */ }
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const expired = !Number.isFinite(held.at) || (now - held.at) > STALE_MS;
|
|
97
|
+
const dead = pidGone(held.pid);
|
|
98
|
+
if (!expired && !dead) return;
|
|
99
|
+
|
|
100
|
+
// Re-read immediately before unlinking and confirm it is still the same
|
|
101
|
+
// holder. Without this, breaking a stale lock could delete a brand new one
|
|
102
|
+
// taken microseconds earlier by a healthy process.
|
|
103
|
+
const still = readHolder(file);
|
|
104
|
+
if (still?.token !== held.token) return;
|
|
105
|
+
try { fs.unlinkSync(file); } catch { /* someone else broke it first */ }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function release(file, token) {
|
|
109
|
+
const held = readHolder(file);
|
|
110
|
+
// Only ever release OUR lock. If the token differs, ours was already broken
|
|
111
|
+
// as stale and someone else legitimately holds it now; unlinking would evict
|
|
112
|
+
// an innocent holder.
|
|
113
|
+
if (held && held.token !== token) return false;
|
|
114
|
+
try { fs.unlinkSync(file); return true; } catch { return false; }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Run `fn` holding the project's lock. Cross-process safe.
|
|
119
|
+
*
|
|
120
|
+
* Keep the body SHORT: a read, a mutate, a rename. Never await network, never
|
|
121
|
+
* spawn, never call another locked helper (the lock is not reentrant, and a
|
|
122
|
+
* nested acquire would deadlock against itself until the stale timeout).
|
|
123
|
+
*/
|
|
124
|
+
export async function withProjectLock(projectDir, fn, { label = '', timeoutMs = ACQUIRE_TIMEOUT_MS } = {}) {
|
|
125
|
+
const file = lockPath(projectDir);
|
|
126
|
+
const deadline = Date.now() + timeoutMs;
|
|
127
|
+
let attempt = 0;
|
|
128
|
+
|
|
129
|
+
for (;;) {
|
|
130
|
+
const token = tryAcquire(file, label);
|
|
131
|
+
if (token) {
|
|
132
|
+
try {
|
|
133
|
+
return await fn();
|
|
134
|
+
} finally {
|
|
135
|
+
release(file, token);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
breakIfStale(file);
|
|
140
|
+
if (Date.now() >= deadline) {
|
|
141
|
+
const held = readHolder(file);
|
|
142
|
+
throw new Error(
|
|
143
|
+
`could not lock ${projectDir} within ${timeoutMs}ms` +
|
|
144
|
+
(held ? ` (held by pid ${held.pid}${held.label ? ` for ${held.label}` : ''} since ${new Date(held.at).toISOString()})` : ''),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
await sleep(BACKOFF_MS[Math.min(attempt++, BACKOFF_MS.length - 1)]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// A real blocking sleep, for the synchronous variant below. Atomics.wait on a
|
|
152
|
+
// buffer nobody will ever notify parks the thread for the duration instead of
|
|
153
|
+
// spinning the CPU, which a busy loop would do.
|
|
154
|
+
const PARK = new Int32Array(new SharedArrayBuffer(4));
|
|
155
|
+
function sleepSync(ms) {
|
|
156
|
+
Atomics.wait(PARK, 0, 0, ms);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* The synchronous twin, for callers that cannot become async.
|
|
161
|
+
*
|
|
162
|
+
* This exists for exactly one reason: index.json's read-modify-write lives in
|
|
163
|
+
* updateIndex, which is synchronous and is called from roughly thirty places
|
|
164
|
+
* across the app. Making it async would ripple through all of them for no
|
|
165
|
+
* benefit, and a half-migrated version of that ripple is a worse risk than a
|
|
166
|
+
* blocking wait measured in milliseconds.
|
|
167
|
+
*
|
|
168
|
+
* It blocks the event loop while waiting, so the same rule as the async version
|
|
169
|
+
* applies with more force: KEEP THE BODY SHORT. Read, mutate, rename. Never do
|
|
170
|
+
* IO that can hang here.
|
|
171
|
+
*/
|
|
172
|
+
export function withProjectLockSync(projectDir, fn, { label = '', timeoutMs = ACQUIRE_TIMEOUT_MS } = {}) {
|
|
173
|
+
const file = lockPath(projectDir);
|
|
174
|
+
const deadline = Date.now() + timeoutMs;
|
|
175
|
+
let attempt = 0;
|
|
176
|
+
|
|
177
|
+
for (;;) {
|
|
178
|
+
const token = tryAcquire(file, label);
|
|
179
|
+
if (token) {
|
|
180
|
+
try {
|
|
181
|
+
return fn();
|
|
182
|
+
} finally {
|
|
183
|
+
release(file, token);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
breakIfStale(file);
|
|
188
|
+
if (Date.now() >= deadline) {
|
|
189
|
+
const held = readHolder(file);
|
|
190
|
+
throw new Error(
|
|
191
|
+
`could not lock ${projectDir} within ${timeoutMs}ms` +
|
|
192
|
+
(held ? ` (held by pid ${held.pid}${held.label ? ` for ${held.label}` : ''} since ${new Date(held.at).toISOString()})` : ''),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
sleepSync(BACKOFF_MS[Math.min(attempt++, BACKOFF_MS.length - 1)]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Exported for tests and for a diagnostic surface; not part of normal use.
|
|
200
|
+
export const _internals = { lockPath, readHolder, breakIfStale, tryAcquire, release, pidGone };
|