@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.
- package/AGENTS.md +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- 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
|