@junghanacs/entwurf 0.20.1 → 0.22.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 +85 -190
- package/BASELINE.md +6 -3
- package/CHANGELOG.md +473 -14
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +332 -60
- package/README.md +100 -22
- package/VERIFY.md +93 -7
- package/docs/acp-backend-rail.md +0 -1
- package/docs/external-mcp-host.md +64 -33
- package/docs/setup-clean-host.md +151 -17
- package/mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js +130 -114
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-caller-seat.js +174 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +519 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-fact-provider.js +22 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-peers-render.js +3 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-contract.js +6 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-native-push.js +30 -17
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-production.js +7 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-runner.js +3 -2
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-send.js +8 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/entwurf-v2-surface.js +7 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-mailbox-body.js +20 -11
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/meta-sender-identity.js +227 -1
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/mux-fresh-call.js +189 -19
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/adapter.js +10 -7
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +403 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/register.js +4 -4
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-hook-codex.js +325 -0
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-path.js +30 -0
- package/mcp/entwurf-bridge/dist/scripts/meta-bridge-fresh-cut.js +6 -1
- package/mcp/entwurf-bridge/src/index.ts +154 -116
- package/mcp/entwurf-bridge/tsconfig.build.json +10 -0
- package/package.json +13 -4
- package/pi-extensions/entwurf-control.ts +71 -19
- package/pi-extensions/lib/codex-caller-seat.ts +204 -0
- package/pi-extensions/lib/codex-fresh-preflight.ts +580 -0
- package/pi-extensions/lib/compaction-send-guard.ts +80 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +29 -3
- package/pi-extensions/lib/entwurf-peers-render.ts +4 -1
- package/pi-extensions/lib/entwurf-v2-contract.ts +6 -7
- package/pi-extensions/lib/entwurf-v2-native-push.ts +35 -18
- package/pi-extensions/lib/entwurf-v2-production.ts +10 -3
- package/pi-extensions/lib/entwurf-v2-runner.ts +3 -2
- package/pi-extensions/lib/entwurf-v2-send.ts +16 -11
- package/pi-extensions/lib/entwurf-v2-surface.ts +7 -4
- package/pi-extensions/lib/meta-mailbox-body.ts +22 -13
- package/pi-extensions/lib/meta-sender-identity.ts +305 -0
- package/pi-extensions/lib/mux-fresh-call.ts +233 -29
- package/pi-extensions/lib/native-push/adapter.ts +21 -24
- package/pi-extensions/lib/native-push/codex-ws-client.ts +506 -0
- package/pi-extensions/lib/native-push/register.ts +7 -9
- package/pi-extensions/meta-bridge-hook-codex.ts +371 -0
- package/run.sh +342 -28
- package/scripts/check-agy-sender-identity.ts +1 -1
- package/scripts/check-codex-app-server-launch.ts +445 -0
- package/scripts/check-codex-birth-hook.ts +264 -0
- package/scripts/check-codex-bridge-identity.ts +179 -0
- package/scripts/check-codex-native-push.ts +386 -0
- package/scripts/check-codex-sender-identity.ts +495 -0
- package/scripts/check-compaction-send-guard.ts +130 -0
- package/scripts/check-copilot-receive-arm.ts +4 -1
- package/scripts/check-entwurf-fact-provider.ts +38 -0
- package/scripts/check-entwurf-peers-surface.ts +13 -1
- package/scripts/check-entwurf-self-address.ts +15 -16
- package/scripts/check-entwurf-v2-contract.ts +4 -3
- package/scripts/check-entwurf-v2-decider.ts +7 -5
- package/scripts/check-entwurf-v2-native-push.ts +35 -7
- package/scripts/check-entwurf-v2-production.ts +245 -12
- package/scripts/check-entwurf-v2-runner.ts +1 -1
- package/scripts/check-entwurf-v2-send.ts +26 -7
- package/scripts/check-entwurf-v2-surface.ts +1 -1
- package/scripts/check-gate-qualification.ts +8 -3
- package/scripts/check-harness-admission-parity.ts +0 -1
- package/scripts/check-mux-launch-tmux.ts +345 -4
- package/scripts/check-native-push-adapter.ts +20 -16
- package/scripts/check-native-push-register.ts +5 -1
- package/scripts/check-release-gate-outcomes.ts +47 -1
- package/scripts/check-setup-qualification.sh +3 -1
- package/scripts/codex-app-server-launch.sh +275 -0
- package/scripts/codex-birth-doctor.sh +276 -0
- package/scripts/codex-birth-install.sh +414 -0
- package/scripts/codex-birth-uninstall.sh +170 -0
- package/scripts/codex-mcp-config.py +435 -0
- package/scripts/codex-socket-path.ts +33 -0
- package/scripts/codex-statusline-config.py +434 -0
- package/scripts/codex-terminal-title-config.py +500 -0
- package/scripts/codex_toml_io.py +653 -0
- package/scripts/lib/codex-fresh-live-protocol.ts +121 -0
- package/scripts/lib/codex-fresh-source-receipts.ts +426 -0
- package/scripts/lib/launch-receipt-windows.ts +46 -0
- package/scripts/lib/tmux-coordinate-row.ts +58 -0
- package/scripts/meta-bridge-fresh-cut.ts +6 -1
- package/scripts/mutants/codex-app-server-launch.json +157 -0
- package/scripts/mutants/codex-caller-seat.json +336 -0
- package/scripts/mutants/codex-native.json +838 -0
- package/scripts/mutants/compaction-send-guard.json +103 -0
- package/scripts/mutants/entwurf-peers.json +19 -0
- package/scripts/mutants/mux-fresh-call.json +93 -10
- package/scripts/mutants/omp-fresh.json +6 -4
- package/scripts/mutants/release-gate.json +13 -0
- package/scripts/mutants/v2-surface.json +75 -1
- package/scripts/raw-async-delivery/README.md +2 -1
- package/scripts/raw-codex-measure/README.md +114 -46
- package/scripts/smoke-agy-native-push-live.ts +3 -1
- package/scripts/smoke-codex-birth.sh +347 -0
- package/scripts/smoke-codex-config-state.sh +700 -0
- package/scripts/smoke-codex-fresh-live.ts +1426 -0
- package/scripts/smoke-codex-native-push-live.ts +75 -0
- package/scripts/smoke-entwurf-chain-live.ts +50 -0
- package/scripts/smoke-setup-verdict.sh +125 -10
- package/scripts/tsconfig.json +1 -0
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { parse } from "smol-toml";
|
|
5
|
+
import { checkCodexSocketFile, realCodexProtocolOpener, resolveCodexDefaultSocketPath, } from "./native-push/codex-ws-client.js";
|
|
6
|
+
export const CODEX_PREFLIGHT_HINT = {
|
|
7
|
+
"codex-birth-unit-missing": "the Codex SessionStart birth unit is absent, drifted from its recorded digests, or unsafely owned; run `entwurf install-codex-birth`, then `entwurf doctor-codex-birth`",
|
|
8
|
+
"codex-birth-trust-missing": "the vendor has recorded no trust receipt for this birth declaration, so the hook is declared and never runs; open a visible plain Codex, answer its prompt with `Trust all and continue`, send one first turn, then `entwurf doctor-codex-birth`",
|
|
9
|
+
"codex-mcp-hand-missing": "the Codex user config does not expose the managed entwurf bridge; run `entwurf install-codex-mcp`, then `entwurf doctor-codex-mcp`",
|
|
10
|
+
"codex-visible-identity-missing": "Codex tui.status_line does not include thread-title; run `entwurf install-codex-statusline`, then `entwurf doctor-codex-statusline`",
|
|
11
|
+
"codex-app-server-unavailable": "the operator-owned Codex app-server default socket is absent or unsafe; start it with `entwurf codex-app-server` from a detached tmux session (one that is NOT where you work), then retry",
|
|
12
|
+
};
|
|
13
|
+
export const CODEX_LAUNCH_CWD_PREFLIGHT_HINT = {
|
|
14
|
+
"codex-launch-cwd-undecided": "Codex has recorded NO decision for the directory this sibling would start in, so the window would open on the vendor's folder-consent screen and WAIT for a human instead of running its first turn — no rollout, no callback, and nothing to address; either launch in a directory this Codex has already been answered for, or open a plain `codex -C <dir>` there once and answer it, then retry",
|
|
15
|
+
"codex-launch-cwd-untrusted-ancestor": "this directory sits inside a project the operator marked `untrusted`, and on this rail the vendor does not offer a consent screen for that — it refuses the startup outright with `pass the repository root explicitly with --cd`, so answering a prompt at this directory would only reproduce that error; launch at that untrusted project's own root instead, or change that project's recorded decision",
|
|
16
|
+
};
|
|
17
|
+
export const CODEX_CALLER_PREFLIGHT_HINT = {
|
|
18
|
+
"codex-caller-title-missing": "this Codex caller's tui.terminal_title does not include thread-id, so the multiplexer reports no pane title naming this thread and there is no caller seat to open the sibling beside; run `entwurf install-codex-terminal-title`, then `entwurf doctor-codex-terminal-title` (an explicit placement.tmuxSession skips this check entirely, because it never needs the seat)",
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* The unit's paths are the operator's own, derived from the SAME environment the installer
|
|
22
|
+
* reads — never a system location and never a flag. A preflight that resolved them any other
|
|
23
|
+
* way could pass while the installed unit sat somewhere else.
|
|
24
|
+
*/
|
|
25
|
+
function unitPaths(env) {
|
|
26
|
+
const home = env.HOME?.trim() ?? "";
|
|
27
|
+
const codexHome = env.CODEX_HOME?.trim() || path.join(home, ".codex");
|
|
28
|
+
const dataHome = env.XDG_DATA_HOME?.trim() || path.join(home, ".local", "share");
|
|
29
|
+
const unitRoot = path.join(dataHome, "entwurf", "codex-birth");
|
|
30
|
+
return {
|
|
31
|
+
hooksFile: path.join(codexHome, "hooks.json"),
|
|
32
|
+
helperDir: path.join(unitRoot, "helper"),
|
|
33
|
+
stateFile: path.join(unitRoot, "install-state.json"),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const CODEX_MCP_ENV_VARS = [
|
|
37
|
+
"CODEX_HOME",
|
|
38
|
+
"ENTWURF_DIR",
|
|
39
|
+
"PI_CODING_AGENT_DIR",
|
|
40
|
+
"ENTWURF_META_SESSIONS_DIR",
|
|
41
|
+
"ENTWURF_META_MAILBOX_DIR",
|
|
42
|
+
"ENTWURF_META_SENDERS_DIR",
|
|
43
|
+
"ENTWURF_META_RECEIVERS_DIR",
|
|
44
|
+
"TMUX",
|
|
45
|
+
"TMUX_PANE",
|
|
46
|
+
];
|
|
47
|
+
function isSafeOwnedFile(file, expectedUid, executable = false) {
|
|
48
|
+
try {
|
|
49
|
+
const stat = fs.lstatSync(file);
|
|
50
|
+
return (stat.isFile() &&
|
|
51
|
+
!stat.isSymbolicLink() &&
|
|
52
|
+
stat.uid === expectedUid &&
|
|
53
|
+
(stat.mode & 0o022) === 0 &&
|
|
54
|
+
(!executable || (stat.mode & 0o111) !== 0));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isSafeOwnedDir(dir, expectedUid) {
|
|
61
|
+
try {
|
|
62
|
+
const stat = fs.lstatSync(dir);
|
|
63
|
+
return stat.isDirectory() && !stat.isSymbolicLink() && stat.uid === expectedUid && (stat.mode & 0o022) === 0;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The unit's own ownership state, read as the digest inventory it is. Bytes that merely
|
|
71
|
+
* EXIST prove nothing: the launcher codex is about to exec must be the one this unit
|
|
72
|
+
* published, so every recorded member is compared to its recorded digest before a sibling
|
|
73
|
+
* is opened. No digest is computed for the VENDOR here — that is a different axis below.
|
|
74
|
+
*/
|
|
75
|
+
function closureDriftedFromState(paths, expectedUid) {
|
|
76
|
+
// The directory holding the state carries the state's authority: anyone who can write it
|
|
77
|
+
// can replace the inventory every digest below is compared against.
|
|
78
|
+
if (!isSafeOwnedDir(path.dirname(paths.stateFile), expectedUid))
|
|
79
|
+
return true;
|
|
80
|
+
if (!isSafeOwnedDir(path.dirname(path.dirname(paths.stateFile)), expectedUid))
|
|
81
|
+
return true;
|
|
82
|
+
if (!isSafeOwnedFile(paths.stateFile, expectedUid))
|
|
83
|
+
return true;
|
|
84
|
+
let state;
|
|
85
|
+
try {
|
|
86
|
+
state = JSON.parse(fs.readFileSync(paths.stateFile, "utf8"));
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (state.schema !== "codex-birth-install-state/v1" || state.status !== "installed")
|
|
92
|
+
return true;
|
|
93
|
+
if (state.hooksFile !== paths.hooksFile || state.helperDir !== paths.helperDir)
|
|
94
|
+
return true;
|
|
95
|
+
const digest = (file) => {
|
|
96
|
+
try {
|
|
97
|
+
return crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
if (typeof state.hooksSha256 !== "string" || digest(paths.hooksFile) !== state.hooksSha256)
|
|
104
|
+
return true;
|
|
105
|
+
const members = state.helperFiles;
|
|
106
|
+
if (!Array.isArray(members))
|
|
107
|
+
return true;
|
|
108
|
+
// Exactly the closure the installer publishes: a short inventory would leave a member
|
|
109
|
+
// nothing compares, which is the same hole as not checking digests at all.
|
|
110
|
+
const named = members.map((raw) => raw?.path);
|
|
111
|
+
const expected = [
|
|
112
|
+
"codex-birth-launch.sh",
|
|
113
|
+
"meta-bridge-hook-codex.ts",
|
|
114
|
+
"lib/meta-session.ts",
|
|
115
|
+
"lib/native-push/codex-ws-client.ts",
|
|
116
|
+
"lib/session-id.js",
|
|
117
|
+
"entwurf-capabilities.json",
|
|
118
|
+
];
|
|
119
|
+
if (named.length !== expected.length)
|
|
120
|
+
return true;
|
|
121
|
+
if (expected.some((name) => !named.includes(name)))
|
|
122
|
+
return true;
|
|
123
|
+
if (named.some((name, index) => named.indexOf(name) !== index))
|
|
124
|
+
return true;
|
|
125
|
+
for (const raw of members) {
|
|
126
|
+
const member = raw;
|
|
127
|
+
const rel = member.path;
|
|
128
|
+
if (typeof rel !== "string" || rel.length === 0 || rel.startsWith("/") || rel.split("/").includes("..")) {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
if (typeof member.sha256 !== "string" || digest(path.join(paths.helperDir, rel)) !== member.sha256)
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
function birthMissing(paths, expectedUid) {
|
|
137
|
+
const launcher = path.join(paths.helperDir, "codex-birth-launch.sh");
|
|
138
|
+
const directories = [
|
|
139
|
+
path.dirname(paths.hooksFile),
|
|
140
|
+
paths.helperDir,
|
|
141
|
+
path.join(paths.helperDir, "lib"),
|
|
142
|
+
path.join(paths.helperDir, "lib", "native-push"),
|
|
143
|
+
];
|
|
144
|
+
if (directories.some((dir) => !isSafeOwnedDir(dir, expectedUid)))
|
|
145
|
+
return true;
|
|
146
|
+
if (!isSafeOwnedFile(paths.hooksFile, expectedUid))
|
|
147
|
+
return true;
|
|
148
|
+
const closure = [
|
|
149
|
+
[launcher, true],
|
|
150
|
+
[path.join(paths.helperDir, "meta-bridge-hook-codex.ts"), false],
|
|
151
|
+
[path.join(paths.helperDir, "lib", "meta-session.ts"), false],
|
|
152
|
+
[path.join(paths.helperDir, "lib", "native-push", "codex-ws-client.ts"), false],
|
|
153
|
+
[path.join(paths.helperDir, "lib", "session-id.js"), false],
|
|
154
|
+
[path.join(paths.helperDir, "entwurf-capabilities.json"), false],
|
|
155
|
+
];
|
|
156
|
+
if (closure.some(([file, executable]) => !isSafeOwnedFile(file, expectedUid, executable)))
|
|
157
|
+
return true;
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(fs.readFileSync(paths.hooksFile, "utf8"));
|
|
160
|
+
const events = parsed.hooks?.SessionStart;
|
|
161
|
+
if (!Array.isArray(events) || events.length !== 1)
|
|
162
|
+
return true;
|
|
163
|
+
const group = events[0];
|
|
164
|
+
if ("matcher" in group || !Array.isArray(group.hooks) || group.hooks.length !== 1)
|
|
165
|
+
return true;
|
|
166
|
+
const hook = group.hooks[0];
|
|
167
|
+
if (Object.keys(hook).sort().join(",") !== "command,timeout,type")
|
|
168
|
+
return true;
|
|
169
|
+
if (hook.type !== "command" || hook.command !== `'${launcher}'` || hook.timeout !== 30)
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return closureDriftedFromState(paths, expectedUid);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* The vendor's own receipt for THIS declaration, read and never written. The key the vendor
|
|
179
|
+
* writes is `<declaration path>:<event>:<group>:<handler>`; the value must be a
|
|
180
|
+
* `trusted_hash` of the shape `sha256:<64 hex>`. What this asserts is that a receipt EXISTS
|
|
181
|
+
* for our declaration identity — never that the hash is correct, which only the vendor can
|
|
182
|
+
* say, and never by launching Codex to find out.
|
|
183
|
+
*/
|
|
184
|
+
function trustReceiptMissing(config, hooksFile) {
|
|
185
|
+
const hooks = config.hooks;
|
|
186
|
+
if (hooks == null || typeof hooks !== "object" || Array.isArray(hooks))
|
|
187
|
+
return true;
|
|
188
|
+
const state = hooks.state;
|
|
189
|
+
if (state == null || typeof state !== "object" || Array.isArray(state))
|
|
190
|
+
return true;
|
|
191
|
+
const entry = state[`${hooksFile}:session_start:0:0`];
|
|
192
|
+
if (entry == null || typeof entry !== "object" || Array.isArray(entry))
|
|
193
|
+
return true;
|
|
194
|
+
const digest = entry.trusted_hash;
|
|
195
|
+
return typeof digest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(digest);
|
|
196
|
+
}
|
|
197
|
+
function readConfig(env) {
|
|
198
|
+
try {
|
|
199
|
+
const text = fs.readFileSync(path.join(env.CODEX_HOME?.trim() || path.join(env.HOME ?? "", ".codex"), "config.toml"), "utf8");
|
|
200
|
+
const value = parse(text);
|
|
201
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function mcpMissing(config) {
|
|
208
|
+
const servers = config.mcp_servers;
|
|
209
|
+
if (servers == null || typeof servers !== "object" || Array.isArray(servers))
|
|
210
|
+
return true;
|
|
211
|
+
const entry = servers["entwurf-bridge"];
|
|
212
|
+
if (entry == null || typeof entry !== "object" || Array.isArray(entry))
|
|
213
|
+
return true;
|
|
214
|
+
const server = entry;
|
|
215
|
+
if (Object.keys(server).sort().join(",") !== "command,env,env_vars")
|
|
216
|
+
return true;
|
|
217
|
+
const envVars = server.env_vars;
|
|
218
|
+
if (!Array.isArray(envVars) ||
|
|
219
|
+
envVars.length !== CODEX_MCP_ENV_VARS.length ||
|
|
220
|
+
envVars.some((name, index) => name !== CODEX_MCP_ENV_VARS[index]))
|
|
221
|
+
return true;
|
|
222
|
+
const env = server.env;
|
|
223
|
+
if (env == null || typeof env !== "object" || Array.isArray(env))
|
|
224
|
+
return true;
|
|
225
|
+
const serverEnv = env;
|
|
226
|
+
return (server.command !== "entwurf-bridge" ||
|
|
227
|
+
Object.keys(serverEnv).join(",") !== "ENTWURF_BRIDGE_NATIVE_HOST" ||
|
|
228
|
+
serverEnv.ENTWURF_BRIDGE_NATIVE_HOST !== "codex");
|
|
229
|
+
}
|
|
230
|
+
function visibleIdentityMissing(config) {
|
|
231
|
+
const tui = config.tui;
|
|
232
|
+
if (tui == null || typeof tui !== "object" || Array.isArray(tui))
|
|
233
|
+
return true;
|
|
234
|
+
const statusLine = tui.status_line;
|
|
235
|
+
return !Array.isArray(statusLine) || !statusLine.includes("thread-title");
|
|
236
|
+
}
|
|
237
|
+
/** `[tui].terminal_title` membership, the exact axis `entwurf doctor-codex-terminal-title`
|
|
238
|
+
* judges. Same shape as `visibleIdentityMissing` and a DIFFERENT key: `status_line` is what a
|
|
239
|
+
* human reads inside the TUI, `terminal_title` is what the multiplexer reports back as
|
|
240
|
+
* `#{pane_title}`. Neither substitutes for the other. */
|
|
241
|
+
function callerTitleMissing(config) {
|
|
242
|
+
const tui = config.tui;
|
|
243
|
+
if (tui == null || typeof tui !== "object" || Array.isArray(tui))
|
|
244
|
+
return true;
|
|
245
|
+
const terminalTitle = tui.terminal_title;
|
|
246
|
+
return !Array.isArray(terminalTitle) || !terminalTitle.includes("thread-id");
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The CALLER-side capability, pre-mutation and synchronous.
|
|
250
|
+
*
|
|
251
|
+
* Synchronous because it reads one config file and nothing else: there is no app-server axis
|
|
252
|
+
* here and there must not be one — whether the operator's app-server is up says nothing about
|
|
253
|
+
* whether a caller's pane can be found, and asking would make a placement check fail for a
|
|
254
|
+
* delivery reason.
|
|
255
|
+
*
|
|
256
|
+
* Call it only when the anchor will actually be USED — a codex caller that named an explicit
|
|
257
|
+
* `placement` never consults the title, so refusing it for a missing `thread-id` would be a
|
|
258
|
+
* refusal for an unused capability.
|
|
259
|
+
*/
|
|
260
|
+
export function codexCallerFreshPreflight(env) {
|
|
261
|
+
const config = readConfig(env);
|
|
262
|
+
if (config === null || callerTitleMissing(config))
|
|
263
|
+
return "codex-caller-title-missing";
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* The launch directory's own capability, pre-mutation and synchronous — and DELIBERATELY NARROWER
|
|
268
|
+
* than the vendor's own decision, because the leaf that decides here reads one TOML file while the
|
|
269
|
+
* vendor asks its app-server. Read this whole comment as the scope statement it is: what follows
|
|
270
|
+
* says what this answers, and then says what it refuses to claim.
|
|
271
|
+
*
|
|
272
|
+
* WHY THE AXIS EXISTS. `[source rust-v0.153.4]` a fresh call always passes `--remote`, so the TUI
|
|
273
|
+
* resolves `AppServerTarget::Remote` and startup ALWAYS runs `check_directory_trust` on the `-C`
|
|
274
|
+
* value (`tui/src/lib.rs:1699-1725`). Nothing on that path consults the approval or sandbox policy
|
|
275
|
+
* (`tui/src/onboarding/directory_trust.rs:33-130`), which is why the
|
|
276
|
+
* `--dangerously-bypass-approvals-and-sandbox` token this argv already carries does NOT cover it:
|
|
277
|
+
* approvals and folder consent are two different gates. A directory with no recorded decision
|
|
278
|
+
* renders the consent screen and blocks, and a blocked TUI has started no turn — no rollout, no
|
|
279
|
+
* birth, no callback, and the caller waits out its whole timeout on a window that is merely
|
|
280
|
+
* waiting to be answered. `[측정 2026-09-16]` the two release-gate runs that failed launched in
|
|
281
|
+
* `…-2zznHl` and `…-kSsoAn`, neither of which had an entry; the two that passed launched in
|
|
282
|
+
* `…-db65N2` and `…-pBXxOJ`, both of which did. Unattended, the failure is silent.
|
|
283
|
+
*
|
|
284
|
+
* THE THREE VENDOR OUTCOMES THIS LEAF MODELS, in the vendor's own order
|
|
285
|
+
* (`tui/src/config_update.rs:290-371`, `ProjectTrustHost::Remote`):
|
|
286
|
+
*
|
|
287
|
+
* 1. A DIRECT decision on the exact cwd starts a turn, and BOTH recorded answers do. `trusted`
|
|
288
|
+
* returns before any screen (`:346-354`); a saved `untrusted` is explicitly skipped for a
|
|
289
|
+
* remote target — `if target.uses_remote_workspace() && trust_level == Some(Untrusted) {
|
|
290
|
+
* continue; }` (`onboarding/directory_trust.rs:94-96`), and `uses_remote_workspace()` is
|
|
291
|
+
* exactly `matches!(self, Self::Remote { .. })` (`tui/src/lib.rs:307-309`). Refusing a
|
|
292
|
+
* deliberate `untrusted` would be entwurf inventing a policy the vendor does not have.
|
|
293
|
+
* The key is the exact cwd — `cwd_keys = vec![cwd_key]` (`:290-296`), no root marker, no git
|
|
294
|
+
* root, no parent, all three of which exist only on the Local host. `[측정 2026-09-16]`
|
|
295
|
+
* `~/.codex/config.toml` carried a trusted `/tmp` and still recorded two separate
|
|
296
|
+
* `/tmp/entwurf-codex-fresh-live-*` entries, one per run a human answered.
|
|
297
|
+
* 2. With NO direct decision, an enabled PROJECT LAYER consents on the directory's behalf
|
|
298
|
+
* (`trust_level.is_none() && disabled_project.is_none() && project_layers.any(no
|
|
299
|
+
* disabledReason)` → `Ok(None)`, `:346-354`). Those layers come from the app-server's
|
|
300
|
+
* `ConfigRead { include_layers: true }` answer, which this leaf does not have. A layer that
|
|
301
|
+
* is present but DISABLED is a fourth outcome — the vendor preserves an unknown or untrusted
|
|
302
|
+
* project layer as disabled rather than dropping it, and that path falls through to the
|
|
303
|
+
* screen — and this leaf cannot tell the two apart either. So wherever a layer could exist at
|
|
304
|
+
* all it answers `null`, which folds both outcomes into "proceed".
|
|
305
|
+
* 3. With no direct decision and NO layers, a cwd inside an explicitly `untrusted` ancestor is
|
|
306
|
+
* not a consent screen at all: the remote branch returns an ERROR
|
|
307
|
+
* (`"remote project directory is inside an explicitly untrusted project; pass the repository
|
|
308
|
+
* root explicitly with --cd"`, `:357-371`). That is a different failure with a different
|
|
309
|
+
* repair, so it gets its own reason — answering `Trust` at the child would only reproduce the
|
|
310
|
+
* same vendor error. Note the vendor's own precondition there is `project_layers.is_empty()`,
|
|
311
|
+
* which is why this reason is only reachable after step 2 has found no layer anywhere.
|
|
312
|
+
*
|
|
313
|
+
* WHAT THIS LEAF IS FOR, AND WHAT IT IS NOT. It is NOT equivalent to the vendor's judgment and
|
|
314
|
+
* must not be described as asking "the same question": it reads the operator's own `config.toml`,
|
|
315
|
+
* while the vendor reads an EFFECTIVE config — system, managed and cloud layers merged around that
|
|
316
|
+
* user layer (`config/src/loader/mod.rs:258-290`, `:430-460`) — through its app-server.
|
|
317
|
+
*
|
|
318
|
+
* That gap is affordable because NOTHING HERE REFUSES A LAUNCH. `freshCall` prints what this leaf
|
|
319
|
+
* saw and opens the window anyway: the consent screen is self-repairing when a human is there, and
|
|
320
|
+
* one answer teaches the vendor the directory for good. The one caller that treats the answer as a
|
|
321
|
+
* precondition is `smoke-codex-fresh-live`, where nobody is at the keyboard and a named
|
|
322
|
+
* precondition is worth more than a callback timeout — that gate owns one directory, answered
|
|
323
|
+
* once.
|
|
324
|
+
*
|
|
325
|
+
* So read a non-null answer as "the vendor will probably stop here, and this is the repair", never
|
|
326
|
+
* as a verdict. Absence is not evidence either way, and every case this leaf cannot see resolves
|
|
327
|
+
* to `null`.
|
|
328
|
+
*/
|
|
329
|
+
export function codexLaunchCwdFreshPreflight(env, launchCwd) {
|
|
330
|
+
// THREE NON-ANSWERS, AND EVERY ONE OF THEM PROCEEDS. Each is a case where this leaf holds no
|
|
331
|
+
// evidence about what the vendor will do, and a refusal without evidence is exactly the false
|
|
332
|
+
// refusal this axis promises not to produce:
|
|
333
|
+
//
|
|
334
|
+
// - A RELATIVE directory. It cannot match a stored key here, but the vendor does not give up
|
|
335
|
+
// on one — it asks its app-server for a cwd and joins (`config_update.rs:203-224`), so the
|
|
336
|
+
// joined path may well be answered. In production this branch is unreachable anyway: the
|
|
337
|
+
// shared cwd leaf already refuses a non-absolute request as `cwd-not-absolute`, which is a
|
|
338
|
+
// better reason than anything this axis could give.
|
|
339
|
+
// - NO READABLE USER CONFIG. A missing `config.toml` is not "no decisions": the vendor loads
|
|
340
|
+
// an empty user table and merges system, managed and cloud layers around it
|
|
341
|
+
// (`config/src/loader/mod.rs:258-290`, `:430-460`, `:520-610`), any of which can carry the
|
|
342
|
+
// decision or the layer that starts the turn.
|
|
343
|
+
// - NO `projects` TABLE. Same reason: it says the USER layer records nothing, not that the
|
|
344
|
+
// effective config does.
|
|
345
|
+
if (!path.isAbsolute(launchCwd))
|
|
346
|
+
return null;
|
|
347
|
+
const config = readConfig(env);
|
|
348
|
+
if (config === null)
|
|
349
|
+
return null;
|
|
350
|
+
const projects = config.projects;
|
|
351
|
+
if (projects == null || typeof projects !== "object" || Array.isArray(projects)) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
const table = projects;
|
|
355
|
+
const levelOf = (key) => {
|
|
356
|
+
const entry = table[key];
|
|
357
|
+
if (entry == null || typeof entry !== "object" || Array.isArray(entry))
|
|
358
|
+
return null;
|
|
359
|
+
const level = entry.trust_level;
|
|
360
|
+
// An unrecognised value leaves `trust_level` as `None` on the vendor side too, and `None`
|
|
361
|
+
// with no project layer is precisely the case that renders the screen.
|
|
362
|
+
return level === "trusted" || level === "untrusted" ? level : null;
|
|
363
|
+
};
|
|
364
|
+
// Outcome 1: a direct decision, either answer.
|
|
365
|
+
if (levelOf(launchCwd) !== null)
|
|
366
|
+
return null;
|
|
367
|
+
// Outcome 2: a PROJECT LAYER may consent with no entry at all, and this leaf cannot enumerate
|
|
368
|
+
// layers — they come from the app-server's `ConfigRead { include_layers: true }` answer. What
|
|
369
|
+
// it CAN decide is the negative: a `.codex` anywhere from the directory upward is the only
|
|
370
|
+
// place such a layer comes from, so when none exists the vendor's `project_layers` really is
|
|
371
|
+
// empty and the two remaining outcomes below are computable. Where one could exist, answer
|
|
372
|
+
// `null`.
|
|
373
|
+
//
|
|
374
|
+
// EXISTENCE, not ownership, and the weaker predicate is the load-bearing one. The vendor
|
|
375
|
+
// admits a layer on its own terms — an unknown or untrusted one is preserved as a DISABLED
|
|
376
|
+
// layer, not dropped — and none of that consults owner/mode/symlink safety. Asking for a
|
|
377
|
+
// safely-owned directory here would let a layer the vendor sees go unseen by this leaf, which
|
|
378
|
+
// would then synthesise an `untrusted-ancestor` refusal for a launch the vendor was going to
|
|
379
|
+
// run. That is the false refusal this axis must never produce, so the check is the widest
|
|
380
|
+
// thing that still means "a layer could live here".
|
|
381
|
+
//
|
|
382
|
+
// The operator's own CODEX HOME is excluded, and excluding it is what keeps this axis from
|
|
383
|
+
// being a no-op: `~/.codex` is an ancestor of nearly every directory anyone launches a sibling
|
|
384
|
+
// in, and it is the USER config root rather than a project layer — the vendor names those
|
|
385
|
+
// apart (`layer.name.dotCodexFolder` for a project, the home for the user layer). Counting it
|
|
386
|
+
// would answer `null` for every path under `$HOME` and the check would never fire in real use.
|
|
387
|
+
const codexHome = path.resolve(env.CODEX_HOME?.trim() || path.join(env.HOME ?? "", ".codex"));
|
|
388
|
+
for (let dir = launchCwd;;) {
|
|
389
|
+
const candidate = path.join(dir, ".codex");
|
|
390
|
+
if (candidate !== codexHome && fs.existsSync(candidate))
|
|
391
|
+
return null;
|
|
392
|
+
const parent = path.dirname(dir);
|
|
393
|
+
if (parent === dir)
|
|
394
|
+
break;
|
|
395
|
+
dir = parent;
|
|
396
|
+
}
|
|
397
|
+
// Outcome 3: no layer anywhere and an explicitly untrusted ancestor — the vendor's own
|
|
398
|
+
// precondition for that branch is exactly `project_layers.is_empty()`, which the loop above
|
|
399
|
+
// has now established. It is reported as its own failure because its repair is to launch at
|
|
400
|
+
// that root, never to answer a prompt at the child.
|
|
401
|
+
//
|
|
402
|
+
// The MATCH IS NOT THE VENDOR'S and is not claimed to be. The vendor compares path URIs
|
|
403
|
+
// (`LegacyAppPathString` → `PathUri::starts_with`, segment-aware, fail-closed on encoded
|
|
404
|
+
// separators); this compares plain strings on a separator boundary. Where the two could
|
|
405
|
+
// disagree — a key or a cwd that is not a plain POSIX path — this answers `undecided` rather
|
|
406
|
+
// than `untrusted-ancestor`. Be exact about what that buys: BOTH are refusals, so this is NOT
|
|
407
|
+
// the launch-permissive direction. What it weakens is the SPECIFIC-ERROR claim — the ancestor
|
|
408
|
+
// reason names another directory as the repair, and naming the wrong one is worse than saying
|
|
409
|
+
// "no decision here". Neither vendor outcome on this branch starts a turn, so the guarantee
|
|
410
|
+
// above is untouched either way.
|
|
411
|
+
const plainPosixPath = (value) => !value.includes("%") && !value.includes("\\") && !value.split("/").some((seg) => seg === "." || seg === "..");
|
|
412
|
+
if (plainPosixPath(launchCwd)) {
|
|
413
|
+
for (const key of Object.keys(table)) {
|
|
414
|
+
if (levelOf(key) !== "untrusted" || !plainPosixPath(key))
|
|
415
|
+
continue;
|
|
416
|
+
if (launchCwd === key || launchCwd.startsWith(key.endsWith("/") ? key : `${key}/`)) {
|
|
417
|
+
return "codex-launch-cwd-untrusted-ancestor";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return "codex-launch-cwd-undecided";
|
|
422
|
+
}
|
|
423
|
+
const DEFAULT_APP_SERVER_TIMEOUT_MS = 5_000;
|
|
424
|
+
function withTimeout(promise, timeoutMs) {
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
const timer = setTimeout(() => reject(new Error("codex app-server exchange timed out")), timeoutMs);
|
|
427
|
+
promise.then((value) => {
|
|
428
|
+
clearTimeout(timer);
|
|
429
|
+
resolve(value);
|
|
430
|
+
}, (error) => {
|
|
431
|
+
clearTimeout(timer);
|
|
432
|
+
reject(error);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* A bounded real initialize/liveness exchange over the app-server's own JSON-RPC. The inode
|
|
438
|
+
* check above only proves a socket FILE with the right owner and mode exists — a dead
|
|
439
|
+
* app-server's stale socket a new process has not reclaimed yet passes that check identically
|
|
440
|
+
* to a live one. Only a real connection plus a real `initialize` round trip distinguishes them.
|
|
441
|
+
* Any failure — refused/hung connection, a response that never lands within the bound, or a
|
|
442
|
+
* reply the transport itself rejects — answers identically: not live. This is deliberately not
|
|
443
|
+
* `probeCodexThread`: preflight asks "is the app-server there to talk to", never "is a specific
|
|
444
|
+
* thread loaded", so it carries no thread id and no retry/backoff policy.
|
|
445
|
+
*/
|
|
446
|
+
async function appServerIsLive(socketPath, openProtocol, timeoutMs) {
|
|
447
|
+
let protocol;
|
|
448
|
+
const opening = openProtocol.open(socketPath);
|
|
449
|
+
try {
|
|
450
|
+
protocol = await withTimeout(opening, timeoutMs);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// The bound expired (or the connect failed), but a slow open can still LAND afterwards.
|
|
454
|
+
// Nothing else holds that handle, so a late success would leave a live connection to the
|
|
455
|
+
// operator's app-server open for the rest of this process. Close it when it arrives —
|
|
456
|
+
// the answer stays "not live" either way; this only refuses to leak.
|
|
457
|
+
void opening.then((late) => {
|
|
458
|
+
try {
|
|
459
|
+
late.close();
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
// best-effort: the socket may already be gone.
|
|
463
|
+
}
|
|
464
|
+
}, () => {
|
|
465
|
+
// The open failed; there is nothing to close.
|
|
466
|
+
});
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
await withTimeout(protocol.request("initialize", {
|
|
471
|
+
clientInfo: { name: "entwurf-fresh-preflight", title: "entwurf-fresh-preflight", version: "0" },
|
|
472
|
+
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
473
|
+
}), timeoutMs);
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
finally {
|
|
480
|
+
try {
|
|
481
|
+
protocol.close();
|
|
482
|
+
}
|
|
483
|
+
catch {
|
|
484
|
+
// best-effort: the socket may already be gone.
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Async because the app-server axis now performs a real bounded protocol exchange, not just an
|
|
490
|
+
* inode check (see `appServerIsLive`). Every other axis stays synchronous filesystem/config
|
|
491
|
+
* work; only the last `await` point can suspend.
|
|
492
|
+
*/
|
|
493
|
+
export async function codexFreshPreflight(env, deps = {}) {
|
|
494
|
+
const paths = deps.unitPathsOverride ?? unitPaths(env);
|
|
495
|
+
// A relative root would make the declaration's launcher path — the very string the vendor
|
|
496
|
+
// keys its trust receipt to — depend on this process's cwd.
|
|
497
|
+
if (!path.isAbsolute(paths.hooksFile) || !path.isAbsolute(paths.helperDir) || !path.isAbsolute(paths.stateFile)) {
|
|
498
|
+
return "codex-birth-unit-missing";
|
|
499
|
+
}
|
|
500
|
+
if (birthMissing(paths, deps.operatorUid ?? process.getuid?.() ?? -1)) {
|
|
501
|
+
return "codex-birth-unit-missing";
|
|
502
|
+
}
|
|
503
|
+
const config = readConfig(env);
|
|
504
|
+
// The vendor receipt is a SEPARATE reject from our bytes: perfect bytes the vendor will
|
|
505
|
+
// not run and absent bytes are two different repairs, and folding them would send the
|
|
506
|
+
// operator to the installer for something only they can answer in their own Codex.
|
|
507
|
+
if (config === null || trustReceiptMissing(config, paths.hooksFile))
|
|
508
|
+
return "codex-birth-trust-missing";
|
|
509
|
+
if (mcpMissing(config))
|
|
510
|
+
return "codex-mcp-hand-missing";
|
|
511
|
+
if (visibleIdentityMissing(config))
|
|
512
|
+
return "codex-visible-identity-missing";
|
|
513
|
+
const socketPath = resolveCodexDefaultSocketPath(env);
|
|
514
|
+
const socket = (deps.checkSocket ?? checkCodexSocketFile)(socketPath);
|
|
515
|
+
if (!socket.ok)
|
|
516
|
+
return "codex-app-server-unavailable";
|
|
517
|
+
const live = await appServerIsLive(socketPath, deps.openProtocol ?? realCodexProtocolOpener, deps.appServerTimeoutMs ?? DEFAULT_APP_SERVER_TIMEOUT_MS);
|
|
518
|
+
return live ? null : "codex-app-server-unavailable";
|
|
519
|
+
}
|
|
@@ -26,7 +26,7 @@
|
|
|
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
|
-
import { isOutOfSocketDomainGardenIdConflict, resolveFactList, } from "./entwurf-facts.js";
|
|
29
|
+
import { isOutOfSocketDomainGardenIdConflict, resolveFactList, UNOBSERVED_PEER, } from "./entwurf-facts.js";
|
|
30
30
|
import { observePeerFacts } from "./entwurf-peer-observe.js";
|
|
31
31
|
import { isLivenessSupported } from "./entwurf-v2-contract.js";
|
|
32
32
|
import { FRESH_CUT_PRESCRIPTION, listAllMetaIdentities, } from "./meta-session.js";
|
|
@@ -130,9 +130,29 @@ export async function listEntwurfFacts(deps) {
|
|
|
130
130
|
// 4. resolveFactList over CLEAN inputs only. Its throws (duplicate identity /
|
|
131
131
|
// unprobed in-domain citizen) are impossible wiring invariants — left to
|
|
132
132
|
// fire as the last line of defense, never caught here.
|
|
133
|
+
//
|
|
134
|
+
// A peers surface may bound observation to its SAME newest rendered rows. Receiver
|
|
135
|
+
// observation can inspect process state and historical Claude markers, so doing it
|
|
136
|
+
// for every retained record made bounded text grow linearly with history. The generic
|
|
137
|
+
// provider stays unbounded when no limit is supplied. Either way the full clean
|
|
138
|
+
// identity/probe arrays reach the core: certification, duplicate detection,
|
|
139
|
+
// diagnostics, liveness, and the machine payload remain complete. Only an observation
|
|
140
|
+
// not performed becomes `unobserved`.
|
|
133
141
|
const cleanIdentities = identities.filter((i) => !conflictGids.has(i.gardenId));
|
|
134
142
|
const cleanProbes = probes.filter((p) => !conflictGids.has(p.gardenId));
|
|
135
|
-
const
|
|
143
|
+
const observe = deps.observe ?? observePeerFacts;
|
|
144
|
+
let selectedObserve = observe;
|
|
145
|
+
if (deps.observationLimit !== undefined) {
|
|
146
|
+
if (!Number.isSafeInteger(deps.observationLimit) || deps.observationLimit < 0) {
|
|
147
|
+
throw new Error("listEntwurfFacts: observationLimit must be a non-negative safe integer");
|
|
148
|
+
}
|
|
149
|
+
const observedGardenIds = new Set([...cleanIdentities]
|
|
150
|
+
.sort((a, b) => (a.gardenId < b.gardenId ? -1 : a.gardenId > b.gardenId ? 1 : 0))
|
|
151
|
+
.slice(Math.max(0, cleanIdentities.length - deps.observationLimit))
|
|
152
|
+
.map((identity) => identity.gardenId));
|
|
153
|
+
selectedObserve = (identity) => (observedGardenIds.has(identity.gardenId) ? observe(identity) : UNOBSERVED_PEER);
|
|
154
|
+
}
|
|
155
|
+
const facts = resolveFactList(cleanIdentities, cleanProbes, selectedObserve);
|
|
136
156
|
// 5. #50 C4 demotion: a record-less socket is a diagnostic, not a listing
|
|
137
157
|
// section. One diagnostic per socket (subjects aggregate at render, F8);
|
|
138
158
|
// the message is liveness-keyed so same-state sockets group into one line.
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* socketPath rows — the pre-record socket-scan worldview) is gone with the
|
|
23
23
|
* `controlDir` it exposed. Socket paths are dispatch-internal transport.
|
|
24
24
|
*/
|
|
25
|
+
/** Human rows and expensive provider observations share this one presentation budget. */
|
|
26
|
+
export const ENTWURF_PEERS_RENDER_LIMIT = 32;
|
|
25
27
|
// `receiver` and `transcript` are OBSERVED facts, not routing (#101): they say what was
|
|
26
28
|
// found on disk for this citizen, and a caller still asks entwurf_v2 what may be done
|
|
27
29
|
// about it. They earn a column because for every claude-code row `liveness` reads
|
|
@@ -102,7 +104,7 @@ function renderDiagnosticLines(diagnostics) {
|
|
|
102
104
|
}
|
|
103
105
|
return lines;
|
|
104
106
|
}
|
|
105
|
-
function compactLines(lines, max =
|
|
107
|
+
function compactLines(lines, max = ENTWURF_PEERS_RENDER_LIMIT) {
|
|
106
108
|
if (lines.length <= max)
|
|
107
109
|
return lines;
|
|
108
110
|
const omitted = lines.length - max;
|
|
@@ -110,13 +110,12 @@ export function isLivenessSupported(backend) {
|
|
|
110
110
|
}
|
|
111
111
|
// ── Native-push backend domain (봉인 2/4) ───────────────────────────────────
|
|
112
112
|
// A backend whose liveness is measured by the SEPARATE native-push adapter rail (a
|
|
113
|
-
// live app-server
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
export const NATIVE_PUSH_BACKENDS = ["antigravity"];
|
|
113
|
+
// live vendor app-server target probe), NOT a control socket. This domain is DISJOINT
|
|
114
|
+
// from LIVENESS_DOMAIN_BACKENDS: an Antigravity or Codex session is `unsupported` on
|
|
115
|
+
// the socket FACT axis (entwurf_peers) yet independently measured on the native-push
|
|
116
|
+
// axis. The two are separate rails on purpose — check-entwurf-facts pins both sets
|
|
117
|
+
// and asserts their intersection is ∅ (a backend can never be in both domains).
|
|
118
|
+
export const NATIVE_PUSH_BACKENDS = ["antigravity", "codex"];
|
|
120
119
|
export function nativePushSupported(backend) {
|
|
121
120
|
return NATIVE_PUSH_BACKENDS.includes(backend);
|
|
122
121
|
}
|