@junghanacs/entwurf 0.12.9 → 0.13.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 +135 -248
- package/BASELINE.md +46 -7
- package/CHANGELOG.md +35 -0
- package/CONTRIBUTING.md +4 -2
- package/DELIVERY.md +5 -4
- package/README.md +46 -18
- package/VERIFY.md +10 -6
- package/demo/demo.sh +7 -1
- package/docs/setup-clean-host.md +10 -4
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +159 -66
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/backend-adapter.js +148 -5
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/config.js +16 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/models.js +66 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/overlay.js +190 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/acp/tool-surface.js +12 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-core.js +26 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-deliverability.js +10 -6
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +15 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-facts.js +13 -13
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-resume-args.js +31 -41
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-self-address.js +8 -9
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +32 -25
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-decider.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-lock.js +5 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +9 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-spawn-production.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-session.js +17 -20
- package/mcp/entwurf-bridge/src/index.ts +190 -82
- package/package.json +7 -3
- package/pi/settings.reference.json +1 -1
- package/pi-extensions/acp-provider.ts +20 -10
- package/pi-extensions/entwurf-control.ts +69 -27
- package/pi-extensions/lib/acp/augment.ts +60 -2
- package/pi-extensions/lib/acp/backend-adapter.ts +183 -8
- package/pi-extensions/lib/acp/backend.ts +5 -1
- package/pi-extensions/lib/acp/config.ts +19 -5
- package/pi-extensions/lib/acp/engraving.ts +3 -1
- package/pi-extensions/lib/acp/event-mapper.ts +10 -3
- package/pi-extensions/lib/acp/models.ts +69 -7
- package/pi-extensions/lib/acp/overlay.ts +234 -3
- package/pi-extensions/lib/acp/tool-surface.ts +12 -4
- package/pi-extensions/lib/entwurf-core.ts +26 -9
- package/pi-extensions/lib/entwurf-deliverability.ts +10 -6
- package/pi-extensions/lib/entwurf-fact-provider.ts +15 -17
- package/pi-extensions/lib/entwurf-facts.ts +13 -13
- package/pi-extensions/lib/entwurf-resume-args.ts +40 -46
- package/pi-extensions/lib/entwurf-self-address.ts +8 -9
- package/pi-extensions/lib/entwurf-v2-contract-schema.ts +7 -2
- package/pi-extensions/lib/entwurf-v2-contract.ts +32 -25
- package/pi-extensions/lib/entwurf-v2-decider.ts +8 -8
- package/pi-extensions/lib/entwurf-v2-lock.ts +5 -4
- package/pi-extensions/lib/entwurf-v2-production.ts +14 -11
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +3 -1
- package/pi-extensions/lib/meta-session.ts +17 -20
- package/run.sh +200 -62
- package/scripts/agy-bridge-config.py +249 -49
- package/scripts/agy-bridge.sh +59 -14
- package/scripts/check-acp-carrier-augment.ts +34 -2
- package/scripts/check-acp-cortex.ts +668 -0
- package/scripts/check-acp-provider-surface.ts +50 -6
- package/scripts/check-acp-sdk-surface.ts +22 -11
- package/scripts/check-acp-session-reuse.ts +64 -1
- package/scripts/check-agy-permission-matrix.py +655 -0
- package/scripts/check-entwurf-deliverability.ts +9 -6
- package/scripts/check-entwurf-fact-provider.ts +7 -7
- package/scripts/check-entwurf-facts.ts +12 -12
- package/scripts/check-entwurf-resume-args.ts +22 -60
- package/scripts/check-entwurf-self-address.ts +187 -4
- package/scripts/check-entwurf-v2-contract.ts +2 -2
- package/scripts/check-entwurf-v2-production.ts +5 -5
- package/scripts/check-entwurf-v2-spawn-production.ts +6 -2
- package/scripts/check-entwurf-v2-surface.ts +327 -14
- package/scripts/check-gate-qualification.ts +787 -0
- package/scripts/check-meta-identity-consumers.ts +1 -1
- package/scripts/check-probe-cli-shim.ts +879 -0
- package/scripts/check-probe-ordering.ts +2450 -0
- package/scripts/check-shell-quote.ts +5 -4
- package/scripts/fixtures/probe-cli-shim +20 -0
- package/scripts/fixtures/probe-mcp-server.ts +168 -12
- package/scripts/lib/mutation-qualify.ts +794 -0
- package/scripts/lib/probe-acp-turn.ts +207 -0
- package/scripts/lib/probe-cli-shim.ts +464 -0
- package/scripts/lib/probe-cli-target.ts +165 -0
- package/scripts/lib/probe-event-log.ts +383 -0
- package/scripts/lib/probe-verdict.ts +1213 -0
- package/scripts/mutants/acp-augment.json +30 -0
- package/scripts/mutants/acp-cortex.json +196 -0
- package/scripts/mutants/agy-permission.json +144 -0
- package/scripts/mutants/meta-identity.json +17 -0
- package/scripts/mutants/probe-ordering.json +1032 -0
- package/scripts/mutants/self-address.json +59 -0
- package/scripts/mutants/v2-surface.json +87 -0
- package/scripts/smoke-acp-cortex-live.ts +392 -0
- package/scripts/smoke-acp-ordering-probe-live.ts +848 -0
- package/scripts/smoke-acp-raw-turn-live.ts +1 -1
- package/scripts/smoke-acp-v2-send-live.ts +33 -18
- package/scripts/smoke-agy-install-state.sh +205 -20
- package/scripts/smoke-entwurf-v2-matrix-live.ts +3 -2
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +0 -100
- package/scripts/check-entwurf-mailbox-guard.ts +0 -266
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
// ACP plugin — curated
|
|
1
|
+
// ACP plugin — curated model surface (S0 loader/fence slice; cortex rows added
|
|
2
|
+
// in 0.13).
|
|
2
3
|
//
|
|
3
4
|
// entwurf is an ACP *plugin* on the v2 core, NOT a general-purpose
|
|
4
5
|
// Anthropic provider. It deliberately does not expose the full pi-ai model
|
|
5
|
-
// registry — the surface is curated to the
|
|
6
|
-
//
|
|
7
|
-
// the provider/model surface up).
|
|
6
|
+
// registry — the surface is curated to the ids the plugin commits to driving
|
|
7
|
+
// through a real ACP backend.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// Two curated sets live here, one per adapter on the rail
|
|
10
|
+
// (backend-adapter.ts): the unprefixed Claude ids (the reference backend) and
|
|
11
|
+
// the `cortex-` prefixed Snowflake Cortex Code ids (the second backend, landed
|
|
12
|
+
// 0.13.0 — docs/acp-backend-rail.md §6/§11-8). Codex is already a native garden
|
|
13
|
+
// citizen and Gemini/major tools use native, so neither is an ACP backend here.
|
|
14
|
+
// A third governed backend EXTENDS this file the same way — it does not change
|
|
12
15
|
// the pattern.
|
|
13
16
|
|
|
14
17
|
// pi 0.80 migration: the standalone root `getModels()` moved to the deprecated
|
|
@@ -81,6 +84,65 @@ function claudeContextWindow(model: { id: string; contextWindow: number }): numb
|
|
|
81
84
|
return Math.min(model.contextWindow, CLAUDE_CONTEXT_DEFAULT);
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
// ── Cortex (Snowflake Cortex Code) curated surface ──────────────────────────
|
|
88
|
+
//
|
|
89
|
+
// entwurf's ACP rail is backend-extensible: Cortex is the first NON-claude
|
|
90
|
+
// backend to land through it (docs/acp-backend-rail.md §6). pi-ai carries no
|
|
91
|
+
// snowflake/cortex model source, so this surface is HAND-CURATED. Every id
|
|
92
|
+
// carries the reserved `cortex-` prefix (§9-1): the prefix is the SINGLE routing
|
|
93
|
+
// authority (backend-adapter.ts `routeModel`) and keeps the ids off the Claude
|
|
94
|
+
// native ids Cortex routes to (`cortex-claude-sonnet-5` vs the unprefixed
|
|
95
|
+
// `claude-sonnet-5` the claude adapter owns). `routeModel` strips the prefix to
|
|
96
|
+
// recover the native id, which is enforced per-turn via
|
|
97
|
+
// session/set_config_option — never a launch `-m` pin (CP0-M).
|
|
98
|
+
//
|
|
99
|
+
// The 4-row set is the GLG-decided curation (2026-07-29) — a subset of the 11
|
|
100
|
+
// native ids cortex v1.1.52 exposed live when authenticated. A curated id that
|
|
101
|
+
// the running cortex no longer serves fails LOUD at set-model, before the
|
|
102
|
+
// prompt (measured: `Unsupported model: …` names the live set). Adding an id
|
|
103
|
+
// here is the same verify-both-axes commitment as the Claude set — do not
|
|
104
|
+
// extend casually.
|
|
105
|
+
export const CORTEX_MODEL_PREFIX = "cortex-";
|
|
106
|
+
export const SUPPORTED_CORTEX_MODEL_IDS = [
|
|
107
|
+
"cortex-auto",
|
|
108
|
+
"cortex-claude-opus-5",
|
|
109
|
+
"cortex-claude-sonnet-5",
|
|
110
|
+
"cortex-openai-gpt-5.4",
|
|
111
|
+
] as const;
|
|
112
|
+
|
|
113
|
+
// Hand-set conservative context window. Cortex reports the LIVE window via its
|
|
114
|
+
// own ACP session config, not this curated metadata — this is a floor for the
|
|
115
|
+
// registry surface, deliberately not inflated to the Claude 1M.
|
|
116
|
+
const CORTEX_CONTEXT_DEFAULT = 200_000;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The curated Cortex model rows handed to the single `entwurf` provider via
|
|
120
|
+
* `allCuratedModels()`. The two Claude rows ride their OWN registry base
|
|
121
|
+
* (opus-5 / sonnet-5); `auto` (default family Claude) and the GPT row (no pi-ai
|
|
122
|
+
* source) ride the sonnet-5 base as a metadata floor. id / name / contextWindow
|
|
123
|
+
* are overridden; the `cortex-` prefix keeps them from colliding with the
|
|
124
|
+
* Claude curated ids. Same row shape as `curatedClaudeModels()` (AcpModelRow).
|
|
125
|
+
*/
|
|
126
|
+
export function curatedCortexModels() {
|
|
127
|
+
const sonnetBase = requireRegistryModel(ANTHROPIC_MODELS_ALL, "claude-sonnet-5");
|
|
128
|
+
const opusBase = requireRegistryModel(ANTHROPIC_MODELS_ALL, "claude-opus-5");
|
|
129
|
+
const row = (id: string, name: string, base: RegistryModel) => ({
|
|
130
|
+
id,
|
|
131
|
+
name,
|
|
132
|
+
reasoning: base.reasoning,
|
|
133
|
+
input: base.input,
|
|
134
|
+
cost: base.cost,
|
|
135
|
+
contextWindow: CORTEX_CONTEXT_DEFAULT,
|
|
136
|
+
maxTokens: base.maxTokens,
|
|
137
|
+
});
|
|
138
|
+
return [
|
|
139
|
+
row("cortex-auto", "Cortex · Auto", sonnetBase),
|
|
140
|
+
row("cortex-claude-opus-5", "Cortex · Claude Opus 5", opusBase),
|
|
141
|
+
row("cortex-claude-sonnet-5", "Cortex · Claude Sonnet 5", sonnetBase),
|
|
142
|
+
row("cortex-openai-gpt-5.4", "Cortex · OpenAI GPT-5.4", sonnetBase),
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
84
146
|
/**
|
|
85
147
|
* The curated Claude model rows handed to `pi.registerProvider({ models })`.
|
|
86
148
|
* Fail-loud if the anchor is absent from the pi-ai registry.
|
|
@@ -9,9 +9,15 @@
|
|
|
9
9
|
// operator entries a backend needs (credentials, caches, built-in skills)
|
|
10
10
|
// reachable through a TIGHT symlink whitelist — nothing else.
|
|
11
11
|
//
|
|
12
|
-
// Scope (NEXT §스코프 / §S2-scout 핀3):
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// Scope (NEXT §스코프 / §S2-scout 핀3): the block above describes the CLAUDE
|
|
13
|
+
// overlay — `ensureClaudeConfigOverlay` and its symlink whitelist are written for
|
|
14
|
+
// claude-agent-acp's config surface alone. A second backend brings its OWN
|
|
15
|
+
// materializer, and cortex did: `ensureCortexDualHomeOverlay` (+ projectCortexMcpJson /
|
|
16
|
+
// sweepDeadCortexOverlays / cortexOverlayScopeId) lives further down this file with its
|
|
17
|
+
// own D-number rationale — an isolated HOME rather than a redirect knob, because cortex
|
|
18
|
+
// has no CLAUDE_CONFIG_DIR equivalent (§11-8 D2). The two share the module, never the
|
|
19
|
+
// strategy. Codex/Gemini overlays (CODEX_HOME / admin.toml) remain behavior-oracle
|
|
20
|
+
// territory and are out of scope here.
|
|
15
21
|
//
|
|
16
22
|
// Two deliberate divergences from the literal 0.11.0 illustrative comment block
|
|
17
23
|
// (the 0.11.0 CODE already does both — only its top doc-comment drew projects/
|
|
@@ -27,6 +33,7 @@
|
|
|
27
33
|
// which is exactly the "mailbox absence by design" the plugin commits to
|
|
28
34
|
// (no meta-bridge hook on this child's settings surface → no mailbox).
|
|
29
35
|
|
|
36
|
+
import { createHash } from "node:crypto";
|
|
30
37
|
import {
|
|
31
38
|
existsSync,
|
|
32
39
|
lstatSync,
|
|
@@ -41,6 +48,8 @@ import {
|
|
|
41
48
|
import { homedir } from "node:os";
|
|
42
49
|
import { join } from "node:path";
|
|
43
50
|
|
|
51
|
+
import type { AcpMcpServer } from "./config.js";
|
|
52
|
+
|
|
44
53
|
/** Operator's real Claude config dir — the symlink-passthrough SOURCE. */
|
|
45
54
|
export const CLAUDE_REAL_CONFIG_DIR = join(homedir(), ".claude");
|
|
46
55
|
|
|
@@ -218,3 +227,225 @@ export function ensureClaudeConfigOverlay(
|
|
|
218
227
|
}
|
|
219
228
|
}
|
|
220
229
|
}
|
|
230
|
+
|
|
231
|
+
// ============================================================================
|
|
232
|
+
// Cortex dual-HOME overlay — the as-measured containment for `cortex acp serve`
|
|
233
|
+
// (CP0 audit 2026-07-29, Cortex Code v1.1.52; docs/acp-backend-rail.md §4/§11-8).
|
|
234
|
+
// ============================================================================
|
|
235
|
+
//
|
|
236
|
+
// Why this is NOT the claude-shaped `SNOWFLAKE_HOME`-only overlay PR #40 shipped
|
|
237
|
+
// (each point is a measured defect, not a preference — §11-8 D-numbers):
|
|
238
|
+
//
|
|
239
|
+
// - D2: cortex reads `CONFIG_DIRS = [".claude", ".cortex"]` at `homedir()` and
|
|
240
|
+
// `~/.claude/skills` — a SNOWFLAKE_HOME redirect cannot move that axis. An
|
|
241
|
+
// overlay session carrying only auth symlinks still advertised the
|
|
242
|
+
// operator's 42 `~/.claude/skills` entries. Cortex has no
|
|
243
|
+
// `CLAUDE_CONFIG_DIR`-equivalent redirect knob (string occurs 0 times in the
|
|
244
|
+
// binary), so the only containment that closes the leak is an ISOLATED HOME:
|
|
245
|
+
// measured global-scope skill count 0, hook trace 0, bundled/project surface
|
|
246
|
+
// intact. Scope note: `homedir()`-anchored *operator-global* state is what
|
|
247
|
+
// the isolation closes; explicit cwd PROJECT scope (`<cwd>/.claude/*`) is
|
|
248
|
+
// retained by contract — a sibling working in a repo sees that repo's
|
|
249
|
+
// declared project surface.
|
|
250
|
+
// - D3: `CORTEX_HOME` beats `SNOWFLAKE_HOME` in cortex's own resolver, so one
|
|
251
|
+
// ambient operator variable would silently bypass the whole overlay. The
|
|
252
|
+
// adapter REFUSES to spawn when `CORTEX_HOME` is present at all (empty
|
|
253
|
+
// string included) — same presence-refusal family as the ordering probe's
|
|
254
|
+
// `CLAUDE_CODE_EXECUTABLE` precondition (backend-adapter.ts).
|
|
255
|
+
// - D9: cortex ACP `newSession` reads only `cwd` and `_meta` — the wire
|
|
256
|
+
// `mcpServers` param the backend-invariant turn loop passes is IGNORED. The
|
|
257
|
+
// explicit `entwurfProvider.mcpServers` (envelope-enriched) are therefore
|
|
258
|
+
// PROJECTED into the overlay-private `$SNOWFLAKE_HOME/cortex/mcp.json` (the
|
|
259
|
+
// door `cortex mcp add` writes). Exact-author every spawn; an entry type the
|
|
260
|
+
// file cannot represent fails loud BEFORE spawn (no silent drop).
|
|
261
|
+
// - D10: an isolated HOME also cuts `~/.pi/agent` (garden store, sockets,
|
|
262
|
+
// spawn surface) off the bundled entwurf-bridge — tools reach the model but
|
|
263
|
+
// see an EMPTY garden. Dual-HOME closes it: the `entwurf-bridge` mcp.json
|
|
264
|
+
// entry ALONE gets `HOME=<real operator home>` restored; every other MCP
|
|
265
|
+
// child stays in the isolated home. The real home is captured by the parent
|
|
266
|
+
// as an absolute path BEFORE spawn — never re-derived inside the child.
|
|
267
|
+
// - D4: cortex self-updates on launch by default, and `acp serve` accepts no
|
|
268
|
+
// `--no-auto-update` (the global flag position boots a TUI with exit 0 —
|
|
269
|
+
// protocol corruption, not a server). The one remaining door is the overlay
|
|
270
|
+
// writing `"autoUpdate": false` into its own `cortex/settings.json`. That is
|
|
271
|
+
// a mid-turn self-replacement OFF switch, not a version pin.
|
|
272
|
+
// - D5/F: auth passthrough is the measured MINIMUM: `connections.toml`,
|
|
273
|
+
// `config.toml` (optional — absent on the measured host),
|
|
274
|
+
// `cortex/cache/credential_cache` (auth succeeded with exactly this set).
|
|
275
|
+
// The WHOLE `cortex/cache` leaks operator tool_outputs/tip history; operator
|
|
276
|
+
// `cortex/skills` and operator `cortex/mcp.json` are denied outright.
|
|
277
|
+
// Symlink-through only — entwurf never copies/parses/mediates the Snowflake
|
|
278
|
+
// credential (AGENTS §ACP Plugin Boundary, Hard Rule 9).
|
|
279
|
+
//
|
|
280
|
+
// The overlay is SESSION/CHILD-SCOPED, never a static shared dir: two residents
|
|
281
|
+
// with different envelopes/configs would race one mcp.json. Scope id =
|
|
282
|
+
// `<host pid>-<sha256(scopeKey) 12 hex>`; the scope dir is torn down and
|
|
283
|
+
// exact-rewritten on every spawn (the prior child for the key is already dead —
|
|
284
|
+
// backend.ts tears it down before a "new" decision spawns), which is also the
|
|
285
|
+
// memory containment: nothing cortex wrote into the isolated home survives into
|
|
286
|
+
// the next session. Scope dirs whose host pid is gone are swept opportunistically.
|
|
287
|
+
|
|
288
|
+
/** Root under which per-session cortex dual-HOME overlays are materialized. */
|
|
289
|
+
export const CORTEX_OVERLAYS_ROOT = join(homedir(), ".pi", "agent", "cortex-overlays");
|
|
290
|
+
|
|
291
|
+
/** The name of the ONE mcp.json entry whose child gets the real operator HOME
|
|
292
|
+
* restored (D10 dual-HOME). Everything else stays in the isolated home. */
|
|
293
|
+
export const CORTEX_DUAL_HOME_BRIDGE_SERVER = "entwurf-bridge";
|
|
294
|
+
|
|
295
|
+
export interface CortexOverlayParams {
|
|
296
|
+
/** Session/child scope discriminator (PI_SESSION_ID, else a cwd-derived key).
|
|
297
|
+
* Identity for overlay-dir separation only — never an address authority. */
|
|
298
|
+
scopeKey: string;
|
|
299
|
+
/** Normalized + envelope-enriched MCP servers to project into mcp.json. */
|
|
300
|
+
mcpServers: readonly AcpMcpServer[];
|
|
301
|
+
/** Absolute real operator HOME captured by the parent BEFORE spawn (D10). */
|
|
302
|
+
realHome: string;
|
|
303
|
+
/** Real snowflake home (auth source). Defaults to `<realHome>/.snowflake`. */
|
|
304
|
+
realSnowflakeHome?: string;
|
|
305
|
+
/** Overlay root override (tests). Defaults to CORTEX_OVERLAYS_ROOT. */
|
|
306
|
+
overlaysRoot?: string;
|
|
307
|
+
/** pid-liveness probe override (tests) for the dead-scope sweep. */
|
|
308
|
+
isPidAlive?: (pid: number) => boolean;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export interface CortexOverlayResult {
|
|
312
|
+
/** Isolated HOME for the cortex child (spawn env HOME). */
|
|
313
|
+
home: string;
|
|
314
|
+
/** Isolated snowflake home (spawn env SNOWFLAKE_HOME) = `<home>/.snowflake`. */
|
|
315
|
+
snowflakeHome: string;
|
|
316
|
+
/** The scope dir owning both (for diagnostics/teardown). */
|
|
317
|
+
scopeDir: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Deterministic per-(host process, session key) overlay dir name. */
|
|
321
|
+
export function cortexOverlayScopeId(scopeKey: string, pid: number = process.pid): string {
|
|
322
|
+
const digest = createHash("sha256").update(scopeKey).digest("hex").slice(0, 12);
|
|
323
|
+
return `${pid}-${digest}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Overlay-authored `$SNOWFLAKE_HOME/cortex/settings.json` — D4's one door. */
|
|
327
|
+
export function cortexOverlaySettingsJson(): string {
|
|
328
|
+
return `${JSON.stringify({ autoUpdate: false }, null, "\t")}\n`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Project the (envelope-enriched) explicit server list into cortex's
|
|
333
|
+
* `$SNOWFLAKE_HOME/cortex/mcp.json` shape (D9). Only stdio entries are measured
|
|
334
|
+
* through this door — an http/sse entry fails loud BEFORE spawn rather than
|
|
335
|
+
* being silently dropped. The `entwurf-bridge` entry alone carries
|
|
336
|
+
* `HOME=<realHome>` (D10); a bridge-declared HOME env is overridden, not merged.
|
|
337
|
+
*/
|
|
338
|
+
export function projectCortexMcpJson(servers: readonly AcpMcpServer[], realHome: string): string {
|
|
339
|
+
const out: Record<string, { type: "stdio"; command: string; args: string[]; env: Record<string, string> }> = {};
|
|
340
|
+
for (const server of servers) {
|
|
341
|
+
if ("type" in server && (server.type === "http" || server.type === "sse")) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`entwurf: cortex mcp.json projection cannot represent ${server.type} server ` +
|
|
344
|
+
`${JSON.stringify(server.name)} — only stdio entries are measured through ` +
|
|
345
|
+
`$SNOWFLAKE_HOME/cortex/mcp.json (CP0 D9). Remove it from entwurfProvider.mcpServers ` +
|
|
346
|
+
`for cortex models or front it with a stdio bridge.`,
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const stdio = server as { name: string; command: string; args: string[]; env: { name: string; value: string }[] };
|
|
350
|
+
const env: Record<string, string> = {};
|
|
351
|
+
for (const kv of stdio.env) env[kv.name] = kv.value;
|
|
352
|
+
if (stdio.name === CORTEX_DUAL_HOME_BRIDGE_SERVER) env.HOME = realHome;
|
|
353
|
+
out[stdio.name] = { type: "stdio", command: stdio.command, args: [...stdio.args], env };
|
|
354
|
+
}
|
|
355
|
+
return `${JSON.stringify({ mcpServers: out }, null, "\t")}\n`;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function cortexPidAlive(pid: number): boolean {
|
|
359
|
+
try {
|
|
360
|
+
process.kill(pid, 0);
|
|
361
|
+
return true;
|
|
362
|
+
} catch (error) {
|
|
363
|
+
// EPERM = alive but not ours; anything else (ESRCH) = gone.
|
|
364
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Opportunistic GC of overlay scope dirs whose HOST pid is gone (process
|
|
370
|
+
* resources only — the overlay holds no records/transcripts worth preserving;
|
|
371
|
+
* conversations state is contained BY DESIGN, Hard Rule 8 does not apply to it).
|
|
372
|
+
* Entries not matching the scope-id grammar are left alone.
|
|
373
|
+
*/
|
|
374
|
+
export function sweepDeadCortexOverlays(
|
|
375
|
+
overlaysRoot: string,
|
|
376
|
+
isPidAlive: (pid: number) => boolean = cortexPidAlive,
|
|
377
|
+
): void {
|
|
378
|
+
let entries: string[];
|
|
379
|
+
try {
|
|
380
|
+
entries = readdirSync(overlaysRoot);
|
|
381
|
+
} catch {
|
|
382
|
+
return; // root absent — nothing to sweep
|
|
383
|
+
}
|
|
384
|
+
for (const entry of entries) {
|
|
385
|
+
const match = /^(\d+)-[0-9a-f]{12}$/.exec(entry);
|
|
386
|
+
if (!match) continue;
|
|
387
|
+
const pid = Number(match[1]);
|
|
388
|
+
if (pid === process.pid) continue;
|
|
389
|
+
if (isPidAlive(pid)) continue;
|
|
390
|
+
try {
|
|
391
|
+
rmSync(join(overlaysRoot, entry), { recursive: true, force: true });
|
|
392
|
+
} catch {
|
|
393
|
+
// Best-effort GC; a stuck dir is retried on the next spawn.
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Symlink `realPath` at `overlayPath` when the operator actually has it.
|
|
399
|
+
* The scope dir is freshly rebuilt by the caller, so no stale-link repair. */
|
|
400
|
+
function cortexLinkIfExists(realPath: string, overlayPath: string): void {
|
|
401
|
+
if (!existsSync(realPath)) return;
|
|
402
|
+
try {
|
|
403
|
+
symlinkSync(realPath, overlayPath);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.error(
|
|
406
|
+
`[entwurf:cortex-overlay] symlink failed for ${overlayPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Materialize the session-scoped cortex dual-HOME overlay: tear down the scope
|
|
413
|
+
* dir and exact-rewrite it (auth symlinks + authored settings.json/mcp.json).
|
|
414
|
+
* Returns the isolated HOME/SNOWFLAKE_HOME the spawn env must carry.
|
|
415
|
+
*/
|
|
416
|
+
export function ensureCortexDualHomeOverlay(params: CortexOverlayParams): CortexOverlayResult {
|
|
417
|
+
if (!params.realHome || !params.realHome.startsWith("/")) {
|
|
418
|
+
throw new Error(
|
|
419
|
+
`entwurf: cortex dual-HOME overlay requires an absolute realHome captured by the parent (got ${JSON.stringify(params.realHome)})`,
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
const overlaysRoot = params.overlaysRoot ?? CORTEX_OVERLAYS_ROOT;
|
|
423
|
+
const realSnowflake = params.realSnowflakeHome ?? join(params.realHome, ".snowflake");
|
|
424
|
+
sweepDeadCortexOverlays(overlaysRoot, params.isPidAlive ?? cortexPidAlive);
|
|
425
|
+
|
|
426
|
+
const scopeDir = join(overlaysRoot, cortexOverlayScopeId(params.scopeKey));
|
|
427
|
+
// Exact rewrite (never merge): the prior child for this scope key is already
|
|
428
|
+
// torn down (backend.ts closes it before a "new" decision spawns), so
|
|
429
|
+
// everything it wrote — conversations, logs, $HOME dotfiles — is discarded
|
|
430
|
+
// here. rmSync does not follow symlinks, so the real auth files are untouched.
|
|
431
|
+
rmSync(scopeDir, { recursive: true, force: true });
|
|
432
|
+
|
|
433
|
+
const home = join(scopeDir, "home");
|
|
434
|
+
const snowflakeHome = join(home, ".snowflake");
|
|
435
|
+
const cortexDir = join(snowflakeHome, "cortex");
|
|
436
|
+
mkdirSync(join(cortexDir, "cache"), { recursive: true });
|
|
437
|
+
|
|
438
|
+
// D5/F — measured-minimum auth passthrough (symlink-through only).
|
|
439
|
+
cortexLinkIfExists(join(realSnowflake, "connections.toml"), join(snowflakeHome, "connections.toml"));
|
|
440
|
+
cortexLinkIfExists(join(realSnowflake, "config.toml"), join(snowflakeHome, "config.toml"));
|
|
441
|
+
cortexLinkIfExists(
|
|
442
|
+
join(realSnowflake, "cortex", "cache", "credential_cache"),
|
|
443
|
+
join(cortexDir, "cache", "credential_cache"),
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
// D4 — runtime self-replacement off; D9 — explicit-server projection.
|
|
447
|
+
writeFileSync(join(cortexDir, "settings.json"), cortexOverlaySettingsJson(), "utf8");
|
|
448
|
+
writeFileSync(join(cortexDir, "mcp.json"), projectCortexMcpJson(params.mcpServers, params.realHome), "utf8");
|
|
449
|
+
|
|
450
|
+
return { home, snowflakeHome, scopeDir };
|
|
451
|
+
}
|
|
@@ -10,10 +10,18 @@
|
|
|
10
10
|
// if pi has excluded a built-in the backend will still expose, the declared
|
|
11
11
|
// surface diverges from the actual one — we fail fast rather than lie.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
13
|
+
// The module's TWO jobs have different backend scopes — do not collapse them:
|
|
14
|
+
// - `buildClaudeSessionMeta` is claude-only by CONSTRUCTION, with exactly one
|
|
15
|
+
// caller (`claudeAdapter.buildSessionMeta`). Cortex exposes no
|
|
16
|
+
// `_meta.systemPrompt` and no tools/permission declaration surface (native
|
|
17
|
+
// tools + its own overlay mcp.json projection), so its adapter returns
|
|
18
|
+
// `undefined` and that function is never reached for a cortex turn.
|
|
19
|
+
// - `assertExcludeToolsHonored` is BACKEND-INVARIANT: backend.ts runs it for
|
|
20
|
+
// every adapter before any spawn, passing `backend: adapter.backend` and the
|
|
21
|
+
// resolved `config.tools`. A cortex turn DOES enter this module through it.
|
|
22
|
+
// That split is why the `backend` field exists: the honest-divergence logic stays
|
|
23
|
+
// explicit and the matrix gate can exercise both the "claude narrows via tools"
|
|
24
|
+
// and "native always exposes" branches.
|
|
17
25
|
//
|
|
18
26
|
// Carrier guard (NEXT §S2-scout 핀1): `buildClaudeSessionMeta` only attaches
|
|
19
27
|
// `_meta.systemPrompt` when a caller passes one. The shipped S2d path passes the
|
|
@@ -1,14 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* entwurf-core — sync entwurf execution, host-agnostic.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - pi-extensions/entwurf.ts (pi
|
|
6
|
-
* -
|
|
4
|
+
* DIRECT importers, MEASURED from the import graph (2026-07-27) — not from memory:
|
|
5
|
+
* - pi-extensions/entwurf-control.ts (the pi adapter)
|
|
6
|
+
* - pi-extensions/lib/entwurf-v2-spawn-production.ts (v2 spawn-bg launch identity)
|
|
7
|
+
* - pi-extensions/lib/project-trust-handler.ts · lib/acp/augment.ts ·
|
|
8
|
+
* lib/native-push/adapter.ts
|
|
9
|
+
* - plus gates/smokes (check-shell-quote, check-package-source-routing, …)
|
|
10
|
+
*
|
|
11
|
+
* The MCP bridge is a TRANSITIVE consumer, not a direct one, and the distinction is the
|
|
12
|
+
* whole point: `mcp/entwurf-bridge/src/index.ts → entwurf-v2-surface.ts →
|
|
13
|
+
* entwurf-v2-production.ts → entwurf-v2-spawn-production.ts → entwurf-core.ts`. So the old
|
|
14
|
+
* header's claim — that the bridge imports this shared implementation the way a v1 tool
|
|
15
|
+
* surface did — was false, but so is "the bridge does not reach it": it reaches it at
|
|
16
|
+
* runtime, only for spawn-bg production.
|
|
17
|
+
*
|
|
18
|
+
* NOT consumers on any path: `pi-extensions/entwurf.ts` (the v1 pi native tool surface) and
|
|
19
|
+
* `pi-extensions/lib/entwurf-async.ts` were REMOVED in the 0.12 cutover. Do not reintroduce
|
|
20
|
+
* either as a live consumer.
|
|
7
21
|
*
|
|
8
22
|
* This module MUST NOT import anything from @earendil-works/pi-coding-agent or any
|
|
9
|
-
* other pi runtime API. It is pure Node + @sinclair/typebox-free.
|
|
10
|
-
* requires pi's ExtensionAPI (sendMessage, appendEntry, sessionManager) belongs
|
|
11
|
-
*
|
|
23
|
+
* other pi runtime API. It is pure Node + @sinclair/typebox-free. Anything that
|
|
24
|
+
* requires pi's ExtensionAPI (sendMessage, appendEntry, sessionManager) belongs to a
|
|
25
|
+
* pi-adapter surface, never here — the v1 async entwurf path that used to hold that
|
|
26
|
+
* work went away with `pi-extensions/entwurf.ts`.
|
|
12
27
|
*
|
|
13
28
|
* Scope:
|
|
14
29
|
* - sync execution (spawn pi, collect message_end events, return summary)
|
|
@@ -67,9 +82,11 @@ const PI_SETTINGS_PATH = process.env.PI_SETTINGS_PATH
|
|
|
67
82
|
: path.join(AGENT_DIR, "settings.json");
|
|
68
83
|
export const ENTWURF_CODEX_ACP_ENV = "ENTWURF_ACP_FOR_CODEX";
|
|
69
84
|
|
|
70
|
-
// Currently unused: remote/SSH entwurf is fail-fast
|
|
71
|
-
//
|
|
72
|
-
// scripts/check-shell-quote.ts
|
|
85
|
+
// Currently unused: remote/SSH entwurf is fail-fast (garden-native identity is
|
|
86
|
+
// local-FS only). Retained for #11 remote revival and pinned by
|
|
87
|
+
// scripts/check-shell-quote.ts, whose SOURCE_SITES is now THIS FILE ALONE — the
|
|
88
|
+
// entwurf.ts / entwurf-async.ts halves of that former parity check were removed
|
|
89
|
+
// with those files, so there is no cross-file parity left to assert.
|
|
73
90
|
// biome-ignore lint/correctness/noUnusedVariables: retained for #11 remote revival; parity-gated.
|
|
74
91
|
function shellQuote(value: string): string {
|
|
75
92
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* drains its own inbox on wake. A DIRECT-INJECT backend (pi / codex / antigravity)
|
|
19
19
|
* has no mailbox drain at all — enqueuing for it is the SE-1 false success
|
|
20
20
|
* ("✓ delivered" into a void). So deliverable = wakeMode === "self-fetch" AND the
|
|
21
|
-
* receiver is active.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* receiver is active. Every enqueue site must pass this guard before writing a .msg
|
|
22
|
+
* (slice 2d). The shipped sites are the v2 decider and its send-fallback re-resolve;
|
|
23
|
+
* the v1 fallback / MCP v1 / pi-native v1 sites this once also listed were removed in
|
|
24
|
+
* the 0.12 cutover — do not read them as live.
|
|
24
25
|
*
|
|
25
26
|
* - nativePushDeliverable(facts): the SEPARATE deliverability predicate for a
|
|
26
27
|
* NATIVE-PUSH backend (antigravity). A native-push citizen has no mailbox and no
|
|
@@ -89,9 +90,12 @@ export interface ReceiverIdentityFacts {
|
|
|
89
90
|
* Does this presence marker actually belong to the target identity? A marker that is
|
|
90
91
|
* absent, or whose garden id / backend / native session id has drifted from the record,
|
|
91
92
|
* is NOT this receiver — fail-closed (a stale/foreign marker must never raise a dead
|
|
92
|
-
* target to "active"). The single source of truth for "marker ↔ identity match"
|
|
93
|
-
*
|
|
94
|
-
* `
|
|
93
|
+
* target to "active"). The single source of truth for "marker ↔ identity match". Its
|
|
94
|
+
* PRODUCTION consumers are the v2 `mailboxDeliverabilityFor` seam and the MCP bridge's
|
|
95
|
+
* `entwurf_self`. There is no second implementation: `entwurf-mailbox-guard.ts` used to
|
|
96
|
+
* wrap this atom with its own enqueue orchestration, had ZERO production importers (import
|
|
97
|
+
* graph measured 2026-07-27), and was DELETED rather than left as a green gate proving only
|
|
98
|
+
* retired behaviour. A new enqueue site consults this predicate through that seam.
|
|
95
99
|
*/
|
|
96
100
|
export function receiverMarkerMatchesIdentity(
|
|
97
101
|
marker: ReceiverIdentityFacts | null | undefined,
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* (one-way: provider → facts / socket-discovery / meta-session) — no import cycle
|
|
6
6
|
* with `entwurf-facts.ts` (which owns `SocketProbe`/`resolveFactList`).
|
|
7
7
|
*
|
|
8
|
-
* listAllMetaIdentities →
|
|
9
|
-
* → pre-quarantine
|
|
8
|
+
* listAllMetaIdentities → socket-domain gid extraction → scanSocketProbes
|
|
9
|
+
* → pre-quarantine out-of-domain/socket conflicts → resolveFactList(clean)
|
|
10
10
|
* → { facts, diagnostics }
|
|
11
11
|
*
|
|
12
12
|
* Two throw-vs-diagnostics policies, kept distinct (GPT힣 C-원칙):
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
* — that is not a re-implementation of the collision rule, it is input
|
|
21
21
|
* sanitation that leaves the pure-core invariant intact.
|
|
22
22
|
*
|
|
23
|
-
*
|
|
24
|
-
* socket): gardenId is the universal address and a send path
|
|
23
|
+
* An out-of-socket-domain record/socket collision quarantines BOTH sides (the
|
|
24
|
+
* PeerFact AND the socket): gardenId is the universal address and a send path
|
|
25
25
|
* first, so surfacing the record alone (as a clean `unsupported` PeerFact) while
|
|
26
26
|
* a same-gid socket exists would be half a lie. Both leave the normal output;
|
|
27
27
|
* one diagnostic carries the fact. (pi + same-gid socket = the normal merge.)
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import { type FactList,
|
|
30
|
+
import { type FactList, isOutOfSocketDomainGardenIdConflict, resolveFactList } from "./entwurf-facts.ts";
|
|
31
31
|
import { isLivenessSupported } from "./entwurf-v2-contract.ts";
|
|
32
32
|
import {
|
|
33
33
|
type ActiveStoreEntry,
|
|
@@ -130,8 +130,8 @@ export async function listEntwurfFacts(deps: EntwurfFactsDeps): Promise<EntwurfF
|
|
|
130
130
|
// 2. socket axis — probe (dir sockets) ∪ (in-domain citizen canonical paths).
|
|
131
131
|
// Its three hazards (symlink forgery / malformed name / dir-read error) are
|
|
132
132
|
// folded into diagnostics here so the listing survives but never lies.
|
|
133
|
-
const
|
|
134
|
-
const scan = await scanSocketProbes(
|
|
133
|
+
const socketDomainGids = identities.filter((i) => isLivenessSupported(i.backend)).map((i) => i.gardenId);
|
|
134
|
+
const scan = await scanSocketProbes(socketDomainGids, deps.socket ?? {});
|
|
135
135
|
const probes = scan.probes;
|
|
136
136
|
const socketGids = new Set(probes.map((p) => p.gardenId));
|
|
137
137
|
const symlinkedGids = new Set(scan.symlinkedGardenIds);
|
|
@@ -158,24 +158,22 @@ export async function listEntwurfFacts(deps: EntwurfFactsDeps): Promise<EntwurfF
|
|
|
158
158
|
});
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
// 3.
|
|
162
|
-
// predicate is SHARED with
|
|
163
|
-
// listing and dispatch cannot drift
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
// (symlinked) socket survive as a clean PeerFact while the legacy send path
|
|
167
|
-
// still followed the symlink — the gap this closes.
|
|
161
|
+
// 3. Pre-quarantine citizens outside the control-socket capability domain that
|
|
162
|
+
// collide with a control socket. The predicate is SHARED with v2 dispatch so
|
|
163
|
+
// listing and dispatch cannot drift. It unions real and symlinked socket gids:
|
|
164
|
+
// the earlier real-socket-only check let an out-of-domain record survive as a
|
|
165
|
+
// clean PeerFact beside a forged symlink receiver.
|
|
168
166
|
const conflictGids = new Set<string>();
|
|
169
167
|
for (const id of identities) {
|
|
170
|
-
if (
|
|
168
|
+
if (isOutOfSocketDomainGardenIdConflict(id.backend, id.gardenId, socketGids, symlinkedGids)) {
|
|
171
169
|
conflictGids.add(id.gardenId);
|
|
172
170
|
diagnostics.push({
|
|
173
171
|
kind: "garden-id-socket-conflict",
|
|
174
172
|
gardenId: id.gardenId,
|
|
175
173
|
backend: id.backend,
|
|
176
174
|
message:
|
|
177
|
-
`
|
|
178
|
-
"
|
|
175
|
+
`out-of-socket-domain citizen (${id.backend}) shares its gardenId with a control socket ` +
|
|
176
|
+
"(real or symlinked) — address ambiguity; both sides are quarantined from the listing.",
|
|
179
177
|
});
|
|
180
178
|
}
|
|
181
179
|
}
|
|
@@ -75,27 +75,27 @@ export function resolvePeerFact(identity: MetaIdentity, socket: SocketLiveness |
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
* one OR a symlinked/forged one. The gardenId is the universal
|
|
80
|
-
* so a
|
|
81
|
-
* socket
|
|
78
|
+
* An OUT-OF-SOCKET-DOMAIN record whose gardenId collides with a control socket —
|
|
79
|
+
* a real (probed) one OR a symlinked/forged one. The gardenId is the universal
|
|
80
|
+
* address, so a citizen without socket capability sharing it with a socket means
|
|
81
|
+
* the socket reaches a DIFFERENT receiver than the record names — an address split.
|
|
82
82
|
* Both the citizen and the socket are quarantined from the facts listing.
|
|
83
83
|
*
|
|
84
84
|
* The union `socketGids ∪ symlinkedGardenIds` is load-bearing: `socketGids` are
|
|
85
85
|
* gids with a real probed `*.sock`, but `symlinkedGardenIds` are NEVER probed (P1)
|
|
86
86
|
* and so are absent from `socketGids`. Looking at `socketGids` alone (the
|
|
87
|
-
* fact-provider:125 gap this closes) let
|
|
87
|
+
* fact-provider:125 gap this closes) let an out-of-domain citizen with a *symlinked* socket
|
|
88
88
|
* survive as a clean PeerFact while the legacy send path still followed the symlink
|
|
89
89
|
* to a forged receiver. Both axes claim the gid → both must quarantine it.
|
|
90
90
|
*
|
|
91
|
-
* SCOPE: this is the
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
91
|
+
* SCOPE: this is the record-side, out-of-socket-domain conflict only — shared by
|
|
92
|
+
* listing and dispatch so they cannot drift; only the observation-bit source is
|
|
93
|
+
* parameterized. A citizen IN the socket domain whose own canonical socket is a
|
|
94
|
+
* symlink is NOT this predicate's concern — that is a
|
|
95
95
|
* target-specific lstat conflict the decider's `inspectTargetControlSocket` raises
|
|
96
96
|
* as `address-conflict`, kept deliberately separate (GPT 1차 검수 C).
|
|
97
97
|
*/
|
|
98
|
-
export function
|
|
98
|
+
export function isOutOfSocketDomainGardenIdConflict(
|
|
99
99
|
backend: string,
|
|
100
100
|
gardenId: string,
|
|
101
101
|
socketGids: ReadonlySet<string>,
|
|
@@ -163,8 +163,8 @@ export interface FactList {
|
|
|
163
163
|
* `indeterminate` and strand a dormant citizen as un-resumable); a dormant
|
|
164
164
|
* citizen's absent socket file is probed to `dead` (ENOENT) by the wiring and
|
|
165
165
|
* arrives here AS `dead` → dormant → resumable.
|
|
166
|
-
* - out-of-domain citizen WITH a control socket at its gardenId → fail-loud
|
|
167
|
-
* (address ambiguity; a
|
|
166
|
+
* - out-of-socket-domain citizen WITH a control socket at its gardenId → fail-loud
|
|
167
|
+
* (address ambiguity; a backend without socket capability cannot own that rail).
|
|
168
168
|
* - out-of-domain citizen without a socket → `unsupported` (via resolvePeerFact).
|
|
169
169
|
* - a probed gardenId with NO citizen → `RecordLessSocketFact` (#50 C4: a
|
|
170
170
|
* diagnostic subject the provider folds into a `record-less-socket`
|
|
@@ -202,7 +202,7 @@ export function resolveFactList(identities: MetaIdentity[], socketProbes: Socket
|
|
|
202
202
|
if (probeMap.has(gid)) {
|
|
203
203
|
throw new Error(
|
|
204
204
|
`resolveFactList: out-of-domain citizen ${gid} (${identity.backend}) has a control socket — ` +
|
|
205
|
-
"address ambiguity (
|
|
205
|
+
"address ambiguity (an out-of-socket-domain citizen cannot own a control socket)",
|
|
206
206
|
);
|
|
207
207
|
}
|
|
208
208
|
socket = null;
|