@junghanacs/entwurf 0.12.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.
Files changed (183) hide show
  1. package/AGENTS.md +240 -0
  2. package/BASELINE.md +227 -0
  3. package/CHANGELOG.md +1210 -0
  4. package/CONTRIBUTING.md +63 -0
  5. package/DELIVERY.md +209 -0
  6. package/LICENSE +21 -0
  7. package/README.md +504 -0
  8. package/VERIFY.md +260 -0
  9. package/demo/README.md +188 -0
  10. package/demo/demo-baseline.sh +156 -0
  11. package/demo/demo.sh +183 -0
  12. package/docs/assets/entwurf-demo.gif +0 -0
  13. package/docs/assets/entwurf-doomemacs.gif +0 -0
  14. package/docs/assets/entwurf-entwurf.gif +0 -0
  15. package/docs/assets/entwurf-hero.jpg +0 -0
  16. package/docs/setup-clean-host.md +305 -0
  17. package/mcp/entwurf-bridge/src/index.ts +513 -0
  18. package/mcp/entwurf-bridge/start.sh +25 -0
  19. package/mcp/entwurf-bridge/test.sh +54 -0
  20. package/mcp/tsconfig.json +29 -0
  21. package/package.json +130 -0
  22. package/pi/entwurf-capabilities.json +9 -0
  23. package/pi/entwurf-targets.json +20 -0
  24. package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
  25. package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
  26. package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
  27. package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
  28. package/pi/settings.reference.json +43 -0
  29. package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
  30. package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
  31. package/pi-extensions/acp-provider.ts +63 -0
  32. package/pi-extensions/entwurf-control.ts +1692 -0
  33. package/pi-extensions/lib/acp/acp-client.ts +90 -0
  34. package/pi-extensions/lib/acp/augment.ts +238 -0
  35. package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
  36. package/pi-extensions/lib/acp/backend.ts +873 -0
  37. package/pi-extensions/lib/acp/config.ts +552 -0
  38. package/pi-extensions/lib/acp/context.ts +177 -0
  39. package/pi-extensions/lib/acp/engraving.ts +123 -0
  40. package/pi-extensions/lib/acp/event-mapper.ts +339 -0
  41. package/pi-extensions/lib/acp/models.ts +102 -0
  42. package/pi-extensions/lib/acp/overlay.ts +220 -0
  43. package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
  44. package/pi-extensions/lib/acp/session-store.ts +418 -0
  45. package/pi-extensions/lib/acp/tool-surface.ts +184 -0
  46. package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
  47. package/pi-extensions/lib/entwurf-core.ts +2033 -0
  48. package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
  49. package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
  50. package/pi-extensions/lib/entwurf-facts.ts +251 -0
  51. package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
  52. package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
  53. package/pi-extensions/lib/entwurf-preflight.ts +247 -0
  54. package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
  55. package/pi-extensions/lib/entwurf-self-address.ts +117 -0
  56. package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
  57. package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
  58. package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
  59. package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
  60. package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
  61. package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
  62. package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
  63. package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
  64. package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
  65. package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
  66. package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
  67. package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
  68. package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
  69. package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
  70. package/pi-extensions/lib/meta-session.ts +1951 -0
  71. package/pi-extensions/lib/project-trust-handler.ts +154 -0
  72. package/pi-extensions/lib/session-id.js +57 -0
  73. package/pi-extensions/lib/socket-discovery.ts +346 -0
  74. package/pi-extensions/lib/socket-probe.ts +92 -0
  75. package/pi-extensions/meta-bridge-hook.ts +250 -0
  76. package/pi-extensions/model-lock.ts +235 -0
  77. package/prompts/engraving.md +27 -0
  78. package/protocol.js +31 -0
  79. package/run.sh +2832 -0
  80. package/scripts/check-acp-backend-preflight.ts +130 -0
  81. package/scripts/check-acp-carrier-augment.ts +297 -0
  82. package/scripts/check-acp-config.ts +322 -0
  83. package/scripts/check-acp-event-mapper.ts +253 -0
  84. package/scripts/check-acp-overlay.ts +154 -0
  85. package/scripts/check-acp-prompt-builder.ts +191 -0
  86. package/scripts/check-acp-provider-surface.ts +159 -0
  87. package/scripts/check-acp-sdk-surface.ts +211 -0
  88. package/scripts/check-acp-session-reuse.ts +756 -0
  89. package/scripts/check-acp-session-store.ts +387 -0
  90. package/scripts/check-acp-tool-surface.ts +159 -0
  91. package/scripts/check-entwurf-bridge-boot.ts +199 -0
  92. package/scripts/check-entwurf-capabilities.ts +123 -0
  93. package/scripts/check-entwurf-control-rpc.ts +187 -0
  94. package/scripts/check-entwurf-deliverability.ts +158 -0
  95. package/scripts/check-entwurf-fact-provider.ts +316 -0
  96. package/scripts/check-entwurf-facts.ts +358 -0
  97. package/scripts/check-entwurf-mailbox-guard.ts +264 -0
  98. package/scripts/check-entwurf-peers-surface.ts +235 -0
  99. package/scripts/check-entwurf-resume-args.ts +149 -0
  100. package/scripts/check-entwurf-self-address.ts +209 -0
  101. package/scripts/check-entwurf-session-identity.ts +703 -0
  102. package/scripts/check-entwurf-v2-contract.ts +536 -0
  103. package/scripts/check-entwurf-v2-decider.ts +795 -0
  104. package/scripts/check-entwurf-v2-lock.ts +368 -0
  105. package/scripts/check-entwurf-v2-mailbox.ts +228 -0
  106. package/scripts/check-entwurf-v2-matrix.ts +437 -0
  107. package/scripts/check-entwurf-v2-production.ts +406 -0
  108. package/scripts/check-entwurf-v2-release.ts +245 -0
  109. package/scripts/check-entwurf-v2-runner.ts +452 -0
  110. package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
  111. package/scripts/check-entwurf-v2-send.ts +454 -0
  112. package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
  113. package/scripts/check-entwurf-v2-spawn.ts +398 -0
  114. package/scripts/check-entwurf-v2-surface.ts +352 -0
  115. package/scripts/check-keyset-overlap.py +120 -0
  116. package/scripts/check-mailbox-receipt-state.ts +170 -0
  117. package/scripts/check-meta-capability-source.ts +112 -0
  118. package/scripts/check-meta-dual-consumers.ts +154 -0
  119. package/scripts/check-meta-dual-read.ts +158 -0
  120. package/scripts/check-meta-listing.ts +138 -0
  121. package/scripts/check-meta-mailbox-state-write.ts +135 -0
  122. package/scripts/check-meta-migration.ts +212 -0
  123. package/scripts/check-meta-receiver-marker.ts +185 -0
  124. package/scripts/check-meta-record-v2.ts +191 -0
  125. package/scripts/check-meta-session.ts +673 -0
  126. package/scripts/check-model-lock.ts +408 -0
  127. package/scripts/check-package-source-routing.ts +253 -0
  128. package/scripts/check-pi-preflight.ts +304 -0
  129. package/scripts/check-project-trust-handler.ts +265 -0
  130. package/scripts/check-shell-quote.ts +121 -0
  131. package/scripts/check-socket-discovery.ts +428 -0
  132. package/scripts/check-socket-probe.ts +106 -0
  133. package/scripts/fixtures/probe-mcp-server.ts +33 -0
  134. package/scripts/gnew-rpc-drive.ts +211 -0
  135. package/scripts/lib/acp-child-cleanup.ts +116 -0
  136. package/scripts/meta-bridge-doctor.sh +315 -0
  137. package/scripts/meta-bridge-hook-log.sh +26 -0
  138. package/scripts/meta-bridge-install.sh +135 -0
  139. package/scripts/meta-bridge-prune.ts +199 -0
  140. package/scripts/meta-bridge-state.py +549 -0
  141. package/scripts/meta-bridge-statusline.sh +192 -0
  142. package/scripts/meta-bridge-store-doctor.ts +64 -0
  143. package/scripts/meta-bridge-uninstall.sh +39 -0
  144. package/scripts/new-session-id.ts +25 -0
  145. package/scripts/postinstall-chmod.cjs +58 -0
  146. package/scripts/raw-async-delivery/README.md +258 -0
  147. package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
  148. package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
  149. package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
  150. package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
  151. package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
  152. package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
  153. package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
  154. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
  155. package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
  156. package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
  157. package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
  158. package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
  159. package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
  160. package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
  161. package/scripts/resolve-acp-bridge.ts +25 -0
  162. package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
  163. package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
  164. package/scripts/smoke-acp-mcp-live.ts +129 -0
  165. package/scripts/smoke-acp-memory-containment-live.ts +389 -0
  166. package/scripts/smoke-acp-overlay-live.ts +314 -0
  167. package/scripts/smoke-acp-provider-live.ts +162 -0
  168. package/scripts/smoke-acp-raw-turn-live.ts +261 -0
  169. package/scripts/smoke-acp-session-reuse-live.ts +172 -0
  170. package/scripts/smoke-acp-skill-live.ts +144 -0
  171. package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
  172. package/scripts/smoke-claude-native-resume-live.sh +198 -0
  173. package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
  174. package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
  175. package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
  176. package/scripts/smoke-meta-async-drift.sh +171 -0
  177. package/scripts/smoke-meta-honesty.sh +147 -0
  178. package/scripts/smoke-meta-install-state.sh +403 -0
  179. package/scripts/smoke-meta-keyset-guard.sh +111 -0
  180. package/scripts/smoke-meta-prune.sh +174 -0
  181. package/scripts/smoke-resident-garden-guard.sh +433 -0
  182. package/scripts/smoke-session-id-name.ts +187 -0
  183. package/scripts/tsconfig.json +34 -0
@@ -0,0 +1,102 @@
1
+ // ACP plugin — curated Claude model surface (S0 loader/fence slice).
2
+ //
3
+ // entwurf is an ACP *plugin* on the v2 core, NOT a general-purpose
4
+ // Anthropic provider. It deliberately does not expose the full pi-ai model
5
+ // registry — the surface is curated to the Claude anchor the plugin commits to
6
+ // driving through a real ACP backend (the backend lands in S2; S0 only stands
7
+ // the provider/model surface up).
8
+ //
9
+ // Claude-first scope (NEXT §스코프): Codex is already a native garden citizen
10
+ // and Gemini / major tools use native, so the curated surface is Claude-only on
11
+ // this lane. A second governed backend would EXTEND this set — it does not change
12
+ // the pattern.
13
+
14
+ // pi 0.80 migration: the standalone root `getModels()` moved to the deprecated
15
+ // `@earendil-works/pi-ai/compat` entrypoint (the global-API churn). We import it
16
+ // from `/compat` directly.
17
+ //
18
+ // Why `/compat` and NOT the 0.80 provider-factory subpath (the pi-ai
19
+ // providers/anthropic subpath): this file is loaded by pi's
20
+ // EXTENSION loader (pi-coding-agent `core/extensions/loader.ts`), whose jiti
21
+ // alias map resolves ONLY three pi-ai specifiers for extensions — the bare root,
22
+ // `/compat`, and `/oauth` — all to `ai/dist/compat.js`. A `providers/*` subpath
23
+ // is NOT in that map: jiti prefix-matches the bare `@earendil-works/pi-ai` alias
24
+ // and appends the remainder, yielding the unresolvable
25
+ // `…/dist/compat.js/providers/anthropic` (verified live: extension load crash,
26
+ // invisible to static typecheck which resolves against node_modules `exports`).
27
+ // So `/compat` is the SINGLE sanctioned extension entrypoint for the old global
28
+ // model-catalog API, and the SINGLE allowlisted exception in
29
+ // `run.sh check-pi-import-surface`. `getModels` here is compat's deprecated
30
+ // re-export of `getBuiltinModels`. When pi removes compat we migrate to whatever
31
+ // the loader then exposes.
32
+ import { getModels } from "@earendil-works/pi-ai/compat";
33
+
34
+ /** Provider id — current pre-rename surface; S1 renames this load-bearing id to `entwurf`. */
35
+ export const PROVIDER_ID = "entwurf";
36
+
37
+ // #26 auth-boundary sentinel. `pi.registerProvider` requires an apiKey when a
38
+ // provider defines custom models, but the ACP plugin consumes NO key: backend
39
+ // auth belongs to the operator's own Claude CLI child process (AGENTS
40
+ // §Operating boundaries — trust invariants). This lowercase+hyphen literal
41
+ // satisfies pi's auth-present check WITHOUT being read as an ENV reference. An
42
+ // ALL-CAPS value like "ANTHROPIC_API_KEY" would (a) trip pi's legacy-env
43
+ // deprecation and (b) falsely present the plugin as Anthropic-key dependent,
44
+ // failing preflight when the var is unset. Do NOT change to "$ANTHROPIC_API_KEY"
45
+ // — that silences the warning but keeps the wrong auth-boundary shape. The
46
+ // check-auth-boundary gate pins this.
47
+ export const ENTWURF_ACP_NO_AUTH_SENTINEL = "entwurf-no-auth";
48
+
49
+ // The curated Claude ids. Adding one here is a commitment to verify it across
50
+ // both axes (protocol smoke + agent interview) — do not extend casually.
51
+ // Exported so the claude backend adapter (backend-adapter.ts) can answer
52
+ // `routeModel` without re-deriving the set from curatedClaudeModels().
53
+ export const SUPPORTED_ANTHROPIC_MODEL_IDS = ["claude-sonnet-4-6", "claude-opus-4-8"] as const;
54
+
55
+ /** The anchor model whose absence is a hard registry regression, not a soft skip. */
56
+ export const CURATED_ANCHOR_MODEL_ID = "claude-opus-4-8";
57
+
58
+ // Anthropic's registry reports 1M for Claude 4.6+ models, but the public
59
+ // entwurf surface deliberately distinguishes Sonnet vs Opus: Sonnet stays
60
+ // at 200K, Opus surfaces at 1M. (The 0.11.0 ENTWURF_ACP_CLAUDE_CONTEXT env
61
+ // override is a behavior-oracle nicety deferred past S0 — the loader/fence
62
+ // slice only needs the anchor present and registered.)
63
+ const CLAUDE_CONTEXT_DEFAULT = 1_000_000;
64
+ const CLAUDE_SONNET_DEFAULT = 200_000;
65
+
66
+ // `getModels("anthropic")` reads the static builtin model catalog only — no env
67
+ // read, no credential access, no network — preserving the #26 auth-boundary
68
+ // invariant: the curated surface consumes no key.
69
+ const ANTHROPIC_MODELS_ALL = getModels("anthropic");
70
+ type RegistryModel = (typeof ANTHROPIC_MODELS_ALL)[number];
71
+
72
+ function requireRegistryModel(models: readonly RegistryModel[], id: string): RegistryModel {
73
+ const model = models.find((m) => m.id === id);
74
+ // Crash, don't warn (AGENTS): a missing anchor is a genuine pi-ai metadata
75
+ // regression and must fail the curated surface up front rather than be
76
+ // papered over with a fabricated row.
77
+ if (!model) throw new Error(`entwurf: required Claude model missing from pi-ai registry: ${id}`);
78
+ return model;
79
+ }
80
+
81
+ function claudeContextWindow(model: { id: string; contextWindow: number }): number {
82
+ const cap = model.id === "claude-sonnet-4-6" ? CLAUDE_SONNET_DEFAULT : CLAUDE_CONTEXT_DEFAULT;
83
+ return Math.min(model.contextWindow, cap);
84
+ }
85
+
86
+ /**
87
+ * The curated Claude model rows handed to `pi.registerProvider({ models })`.
88
+ * Fail-loud if the anchor is absent from the pi-ai registry.
89
+ */
90
+ export function curatedClaudeModels() {
91
+ const supported = new Set<string>(SUPPORTED_ANTHROPIC_MODEL_IDS);
92
+ requireRegistryModel(ANTHROPIC_MODELS_ALL, CURATED_ANCHOR_MODEL_ID);
93
+ return ANTHROPIC_MODELS_ALL.filter((m) => supported.has(m.id)).map((m) => ({
94
+ id: m.id,
95
+ name: m.name,
96
+ reasoning: m.reasoning,
97
+ input: m.input,
98
+ cost: m.cost,
99
+ contextWindow: claudeContextWindow(m),
100
+ maxTokens: m.maxTokens,
101
+ }));
102
+ }
@@ -0,0 +1,220 @@
1
+ // ACP plugin — Claude config overlay materializer (S2b).
2
+ //
3
+ // claude-agent-acp's SettingsManager loads the operator's `~/.claude/settings.json`
4
+ // DIRECTLY (CLAUDE_CONFIG_DIR is the only knob that redirects that read). So the
5
+ // operator's native `permissions.defaultMode` ("auto"), hooks, plugins, and
6
+ // per-cwd memory/projects state would otherwise leak into entwurf ACP
7
+ // sessions. The overlay redirects SettingsManager at a pi-owned directory whose
8
+ // `settings.json` WE author (minimal, `hooks:{}`), while keeping exactly the
9
+ // operator entries a backend needs (credentials, caches, built-in skills)
10
+ // reachable through a TIGHT symlink whitelist — nothing else.
11
+ //
12
+ // Scope (NEXT §스코프 / §S2-scout 핀3): Claude-only on this lane. Codex/Gemini
13
+ // overlays (CODEX_HOME / admin.toml) are 0.11.0 behavior-oracle territory and
14
+ // out of scope — the bridge backends are not in v2 yet.
15
+ //
16
+ // Two deliberate divergences from the literal 0.11.0 illustrative comment block
17
+ // (the 0.11.0 CODE already does both — only its top doc-comment drew projects/
18
+ // as a symlink): `projects/` and `sessions/` are overlay-PRIVATE empty dirs, not
19
+ // symlinks, so the operator's real ~/.claude/{projects,sessions} is never read
20
+ // or written from an ACP session (this also closes the per-cwd MEMORY.md
21
+ // auto-load leak: the binary finds an empty tree and injects nothing).
22
+ //
23
+ // `hooks:{}` is load-bearing, not cosmetic: the Claude SDK distinguishes an
24
+ // ABSENT hooks key from a configured-but-empty map during organic compaction
25
+ // (the absent shape made a compacting turn emit a meta summary instead of
26
+ // answering — 0.11.0 LIVE probe). Keeping it `{}` inherits NO operator hook —
27
+ // which is exactly the "mailbox absence by design" the plugin commits to
28
+ // (no meta-bridge hook on this child's settings surface → no mailbox).
29
+
30
+ import {
31
+ existsSync,
32
+ lstatSync,
33
+ mkdirSync,
34
+ readdirSync,
35
+ readlinkSync,
36
+ rmSync,
37
+ symlinkSync,
38
+ unlinkSync,
39
+ writeFileSync,
40
+ } from "node:fs";
41
+ import { homedir } from "node:os";
42
+ import { join } from "node:path";
43
+
44
+ /** Operator's real Claude config dir — the symlink-passthrough SOURCE. */
45
+ export const CLAUDE_REAL_CONFIG_DIR = join(homedir(), ".claude");
46
+
47
+ /** pi-owned overlay dir — the CLAUDE_CONFIG_DIR target for ACP child spawns. */
48
+ export const CLAUDE_CONFIG_OVERLAY_DIR = join(homedir(), ".pi", "agent", "claude-config-overlay");
49
+
50
+ /**
51
+ * Operator `~/.claude/` entries exposed to the ACP child via symlink. Anything
52
+ * NOT here is intentionally hidden: CLAUDE.md, hooks, agents, plugins,
53
+ * settings.local.json (personal env / PAT), sessions/projects data, command
54
+ * history, todos — none leak into the model context, hook surface, or env.
55
+ *
56
+ * Limited to: backend auth (`.credentials.json`); the binary's runtime caches +
57
+ * telemetry (cache, debug, session-env, stats-cache.json, statsig, telemetry);
58
+ * the bridge's own scratch surface (shell-snapshots); and built-in
59
+ * (non-operator-defined) skill content (skills). `plugins` is deliberately out —
60
+ * plugin enablement is operator-personal; the plugin set is injected per-session
61
+ * via `_meta.claudeCode.options.plugins`, not filesystem inheritance.
62
+ */
63
+ export const OVERLAY_PASSTHROUGH: ReadonlySet<string> = new Set([
64
+ ".credentials.json",
65
+ "cache",
66
+ "debug",
67
+ "session-env",
68
+ "shell-snapshots",
69
+ "skills",
70
+ "stats-cache.json",
71
+ "statsig",
72
+ "telemetry",
73
+ ]);
74
+
75
+ /**
76
+ * Directories owned by the overlay itself (empty trees). The binary
77
+ * auto-creates and writes per-cwd state under these; an empty overlay-scoped
78
+ * tree keeps operator data at ~/.claude/{projects,sessions} unread/unwritten.
79
+ * Memory containment is NOT provided by this directory shape alone: if the
80
+ * backend preset advertises project memory, Claude can still write overlay-local
81
+ * projects/<cwd>/memory files. The engraving carrier's tiny non-empty preset
82
+ * replacement strips that advertisement; the empty tree is read-isolation and
83
+ * defense-in-depth, not the primary write-containment lever.
84
+ */
85
+ export const OVERLAY_EMPTY_DIRS: ReadonlySet<string> = new Set(["projects", "sessions"]);
86
+
87
+ /**
88
+ * Entries the binary creates INSIDE whatever CLAUDE_CONFIG_DIR it is pointed at
89
+ * (feature cache, `.claude.json` backups). They have no operator-side
90
+ * counterpart, so the cleanup pass preserves real files/dirs here but tears down
91
+ * any STALE symlink (a migration artifact from earlier overlay code that linked
92
+ * every entry). `settings.json` is overlay-authored but listed for symmetry so
93
+ * the cleanup loop never nukes it.
94
+ */
95
+ export const OVERLAY_BINARY_OWNED: ReadonlySet<string> = new Set([".claude.json", "backups", "settings.json"]);
96
+
97
+ /**
98
+ * Minimal overlay settings.json. Only fields with a reason to pin:
99
+ * - `permissions.defaultMode: "default"` neutralizes the operator's native
100
+ * "auto"; combined with the explicit `tools`/`permissionAllow` surface,
101
+ * "default" auto-passes every tool we expose without prompts.
102
+ * - `autoMemoryEnabled: false` — SDK opt-out for auto-memory (defense in
103
+ * depth; the tiny non-empty engraving/preset replacement is the primary
104
+ * write-containment lever for Claude ACP).
105
+ * - `hooks: {}` — configured-but-empty (NOT absent): inherits no operator
106
+ * hook (mailbox absence by design) while keeping the compaction turn honest.
107
+ */
108
+ export function overlaySettingsJson(): string {
109
+ return `${JSON.stringify(
110
+ {
111
+ permissions: { defaultMode: "default" },
112
+ autoMemoryEnabled: false,
113
+ hooks: {},
114
+ },
115
+ null,
116
+ 2,
117
+ )}\n`;
118
+ }
119
+
120
+ /**
121
+ * The launch-env override an ACP child spawn must carry to redirect
122
+ * SettingsManager at the overlay. Pure — merge into the child's `env`.
123
+ */
124
+ export function claudeLaunchEnvDefaults(overlayDir: string = CLAUDE_CONFIG_OVERLAY_DIR): { CLAUDE_CONFIG_DIR: string } {
125
+ return { CLAUDE_CONFIG_DIR: overlayDir };
126
+ }
127
+
128
+ /**
129
+ * Materialize / refresh the Claude config overlay. Idempotent: keeps correct
130
+ * symlinks, replaces wrong ones, removes stale entries cleanly. Safe to call on
131
+ * every ACP session bootstrap.
132
+ */
133
+ export function ensureClaudeConfigOverlay(
134
+ realDir: string = CLAUDE_REAL_CONFIG_DIR,
135
+ overlayDir: string = CLAUDE_CONFIG_OVERLAY_DIR,
136
+ ): void {
137
+ mkdirSync(overlayDir, { recursive: true });
138
+
139
+ // settings.json — always (cheap unconditional rewrite keeps the override in
140
+ // place even if a prior process or operator edited it).
141
+ writeFileSync(join(overlayDir, "settings.json"), overlaySettingsJson(), "utf8");
142
+
143
+ // Empty dirs — overlay-owned; replace any prior symlink with a real dir.
144
+ for (const entry of OVERLAY_EMPTY_DIRS) {
145
+ const overlayPath = join(overlayDir, entry);
146
+ try {
147
+ const existing = lstatSync(overlayPath);
148
+ if (existing.isSymbolicLink() || !existing.isDirectory()) {
149
+ rmSync(overlayPath, { recursive: true, force: true });
150
+ mkdirSync(overlayPath, { recursive: true });
151
+ }
152
+ } catch {
153
+ mkdirSync(overlayPath, { recursive: true });
154
+ }
155
+ }
156
+
157
+ // Symlink passthrough — only whitelisted entries that exist in realDir.
158
+ if (existsSync(realDir)) {
159
+ for (const entry of OVERLAY_PASSTHROUGH) {
160
+ const realPath = join(realDir, entry);
161
+ const overlayPath = join(overlayDir, entry);
162
+
163
+ if (!existsSync(realPath)) {
164
+ // Not present operator-side — remove any stale overlay copy.
165
+ try {
166
+ lstatSync(overlayPath);
167
+ rmSync(overlayPath, { recursive: true, force: true });
168
+ } catch {
169
+ // Doesn't exist — fine.
170
+ }
171
+ continue;
172
+ }
173
+
174
+ try {
175
+ const existing = lstatSync(overlayPath);
176
+ if (existing.isSymbolicLink()) {
177
+ if (readlinkSync(overlayPath) === realPath) continue;
178
+ unlinkSync(overlayPath);
179
+ } else {
180
+ rmSync(overlayPath, { recursive: true, force: true });
181
+ }
182
+ } catch {
183
+ // Doesn't exist — fall through to symlink.
184
+ }
185
+
186
+ try {
187
+ symlinkSync(realPath, overlayPath);
188
+ } catch (error) {
189
+ console.error(
190
+ `[entwurf:claude-overlay] symlink failed for ${entry}: ${error instanceof Error ? error.message : String(error)}`,
191
+ );
192
+ }
193
+ }
194
+ }
195
+
196
+ // Stale cleanup — remove anything off the current allowlist. Binary-owned
197
+ // entries are preserved when real (binary authored them inside the overlay)
198
+ // but torn down when a stale symlink points at operator data.
199
+ for (const entry of readdirSync(overlayDir)) {
200
+ if (OVERLAY_PASSTHROUGH.has(entry)) continue;
201
+ if (OVERLAY_EMPTY_DIRS.has(entry)) continue;
202
+ const overlayPath = join(overlayDir, entry);
203
+
204
+ if (OVERLAY_BINARY_OWNED.has(entry)) {
205
+ try {
206
+ const stat = lstatSync(overlayPath);
207
+ if (stat.isSymbolicLink()) rmSync(overlayPath, { force: true });
208
+ } catch {
209
+ // Doesn't exist — fine; binary creates it on first launch.
210
+ }
211
+ continue;
212
+ }
213
+
214
+ try {
215
+ rmSync(overlayPath, { recursive: true, force: true });
216
+ } catch {
217
+ // Best-effort; a stuck stale entry is annoying but not fatal.
218
+ }
219
+ }
220
+ }
@@ -0,0 +1 @@
1
+ # Engraving Here