@junghanacs/entwurf 0.35.1 → 0.36.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 +2 -2
- package/CHANGELOG.md +209 -0
- package/CONTRIBUTING.md +1 -1
- package/DELIVERY.md +15 -0
- package/FAQ.md +3 -3
- package/README.md +30 -16
- package/VERIFY.md +9 -7
- package/demo/README.md +1 -1
- package/docs/acp-backend-rail.md +1 -1
- package/docs/setup-clean-host.md +30 -22
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/codex-fresh-preflight.js +4 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/native-push/codex-ws-client.js +99 -18
- package/mcp/entwurf-bridge/dist/pi-extensions/lib/pi-durable-runtime.js +3 -3
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +59 -14
- package/mcp/entwurf-bridge/dist/scripts/codex-socket-classify.js +44 -0
- package/mcp/entwurf-bridge/tsconfig.build.json +5 -0
- package/package.json +19 -14
- package/pi/pi-durable/bootstrap.mjs +12 -8
- package/pi/pi-durable/carrier/runtime.js +52 -2
- package/pi/pi-durable/carrier/subagent.js +6 -1
- package/pi/pi-durable/carrier/tui.js +31 -4
- package/pi/pi-durable/carrier-relocation.mjs +2 -2
- package/pi/pi-durable/overlay/upstream-pin.json +79 -36
- package/pi-extensions/lib/codex-fresh-preflight.ts +4 -3
- package/pi-extensions/lib/native-push/codex-ws-client.ts +129 -18
- package/pi-extensions/lib/pi-durable-runtime.ts +4 -4
- package/pi-extensions/meta-bridge-pi-durable.ts +77 -21
- package/run.sh +112 -38
- package/scripts/check-codex-app-server-launch.ts +55 -12
- package/scripts/check-codex-native-push.ts +231 -12
- package/scripts/check-gate-qualification.ts +3 -3
- package/scripts/codex-app-server-launch.sh +54 -29
- package/scripts/codex-socket-classify.ts +46 -0
- package/scripts/emit-pi-durable-carrier.ts +159 -168
- package/scripts/mutants/codex-app-server-launch.json +39 -12
- package/scripts/mutants/codex-native.json +15 -0
- package/scripts/mutants/control-send-receipt.json +1 -1
- package/scripts/mutants/pack-install.json +3 -3
- package/scripts/mutants/pi-durable.json +165 -16
- package/scripts/mutants/pi-floor.json +5 -5
- package/scripts/pi-durable-contact-host.mjs +1 -1
- package/pi/pi-durable/overlay/runtime-contacts.patch +0 -283
|
@@ -46,7 +46,7 @@ import { ToolExecutionComponent, } from "entwurf-pi-dist:mode
|
|
|
46
46
|
import { UserMessageComponent } from "entwurf-pi-dist:modes/interactive/components/user-message.js";
|
|
47
47
|
import { getEditorTheme, getMarkdownTheme, initTheme, theme } from "entwurf-pi-dist:modes/interactive/theme/theme.js";
|
|
48
48
|
import { InteractiveThemeController } from "entwurf-pi-dist:modes/interactive/theme/theme-controller.js";
|
|
49
|
-
import { agentOf,
|
|
49
|
+
import { agentOf, } from "./runtime.js";
|
|
50
50
|
|
|
51
51
|
const SELECT_THEME = {
|
|
52
52
|
selectedPrefix: (text) => theme.fg("accent", text),
|
|
@@ -152,6 +152,26 @@ class CompactionComponent extends Box {
|
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The lines above the editor: first what stays true for as long as it is true, the session's failure and the
|
|
157
|
+
* compaction failure a conversation keeps until a compaction produces a summary, then the latest notices.
|
|
158
|
+
*/
|
|
159
|
+
export function durableNotices(view ) {
|
|
160
|
+
const lines = [];
|
|
161
|
+
if (view.failure !== undefined) {
|
|
162
|
+
const cause = view.failure.error === undefined ? "" : `: ${view.failure.error}`;
|
|
163
|
+
lines.push({
|
|
164
|
+
level: "error",
|
|
165
|
+
message: `Session failed${cause}. It takes no more input, and a tool still running may take a while to stop. Close this session, then reopen it with --continue.`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const compaction = ((view.conversation.docs["pi.live"] ?? {}) ).compactionFailure;
|
|
169
|
+
if (compaction !== undefined) {
|
|
170
|
+
lines.push({ level: "error", message: `Compaction failed (${compaction.reason}): ${compaction.error}` });
|
|
171
|
+
}
|
|
172
|
+
return [...lines, ...view.notices.slice(-4)];
|
|
173
|
+
}
|
|
174
|
+
|
|
155
175
|
/**
|
|
156
176
|
* The live background-task badge: the dock's first row, never shrunk, so live background work stays on
|
|
157
177
|
* screen however small the terminal gets. It counts the graph's background tasks whatever their kind and
|
|
@@ -212,7 +232,13 @@ export function durableLayout(transcript , badge , dock
|
|
|
212
232
|
minSize: 1,
|
|
213
233
|
visible: (viewport) => badge.count === 0 || viewport.height > 1,
|
|
214
234
|
},
|
|
215
|
-
{
|
|
235
|
+
{
|
|
236
|
+
component: new VStack([{ component: badge, shrink: 0 }, ...dock]),
|
|
237
|
+
basis: "auto",
|
|
238
|
+
grow: 0,
|
|
239
|
+
shrink: 1,
|
|
240
|
+
minSize: 1,
|
|
241
|
+
},
|
|
216
242
|
]);
|
|
217
243
|
}
|
|
218
244
|
|
|
@@ -368,7 +394,8 @@ class DurableTui {
|
|
|
368
394
|
this.#syncQueue((view.conversation.docs["pi.inbox"] ?? { items: [] }) );
|
|
369
395
|
this.#syncNotices(view);
|
|
370
396
|
this.#editor.borderColor = theme.getThinkingBorderColor(agentOf(view.conversation).thinkingLevel ?? "off");
|
|
371
|
-
|
|
397
|
+
// A failed session's live state is its last one; nothing is working any more.
|
|
398
|
+
this.#syncStatus(view.failure === undefined ? live : {});
|
|
372
399
|
this.#syncFooter(view);
|
|
373
400
|
if (this.#rebuilt) this.#transcript.scrollToEnd();
|
|
374
401
|
this.#ui.requestRender(this.#rebuilt);
|
|
@@ -409,7 +436,7 @@ class DurableTui {
|
|
|
409
436
|
|
|
410
437
|
#syncNotices(view ) {
|
|
411
438
|
this.#notices.clear();
|
|
412
|
-
for (const item of view
|
|
439
|
+
for (const item of durableNotices(view)) {
|
|
413
440
|
const color = item.level === "error" ? "error" : item.level === "warning" ? "warning" : "muted";
|
|
414
441
|
this.#notices.addChild(new TruncatedText(theme.fg(color, item.message), 1, 0));
|
|
415
442
|
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// rewrite the emitter performs and the resolution the runtime accepts cannot drift apart.
|
|
4
4
|
//
|
|
5
5
|
// The durable app is source-only upstream: published `@earendil-works/pi-coding-agent` ships no
|
|
6
|
-
// `dist/experimental`, and its exports map exposes no deep `dist/` path. The carrier is
|
|
7
|
-
//
|
|
6
|
+
// `dist/experimental`, and its exports map exposes no deep `dist/` path. The carrier is Entwurf's
|
|
7
|
+
// owned app source (pi/pi-durable/src, #141), type-stripped, with exactly two rewrites:
|
|
8
8
|
//
|
|
9
9
|
// "./<sibling>.ts" → "./<sibling>.js" a carrier file importing another carrier file
|
|
10
10
|
// "../../<path>.ts" → entwurf-pi-dist:<path>.js the app importing a pi-coding-agent internal
|
|
@@ -1,64 +1,107 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
3
|
-
"comment": "The
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
"schemaVersion": 3,
|
|
3
|
+
"comment": "The pi-durable carrier: what it is emitted from, what it is, and the published SDK set it runs against. The experimental durable app is source-only upstream (published pi-coding-agent ships no dist/experimental), so Entwurf owns its thin application/TUI composition as canonical source (pi/pi-durable/src) and ships it as a CARRIER: those files, type-stripped, with the relocation in pi/pi-durable/carrier-relocation.mjs. The SDK engine (execution, scheduler, queues, storage/transcript, compaction, cancellation/recovery, providers, auth) stays the published packages in `sdk`. scripts/emit-pi-durable-carrier.ts is the only writer of the generated fields (carrier blob, sha256, emitter.node); an installed package never reads source. Model data rides the published @earendil-works/pi-ai.",
|
|
4
|
+
"origin": {
|
|
5
|
+
"comment": "Frozen history, never an input: the upstream commit the owned source was first taken from, its objects as recorded then, the overlay patch that carried Entwurf's changes until it was folded into the source (#141), and the carrier bytes the last patch-built release shipped. Neither install nor emit reads this; `emit-pi-durable-carrier --origin <clone>` checks it as provenance only.",
|
|
6
|
+
"repository": "https://github.com/earendil-works/pi",
|
|
7
|
+
"tag": "v1.1.0",
|
|
8
|
+
"commit": "abe508e1b89912adde45528136c3221eb69acdd7",
|
|
9
|
+
"license": {
|
|
10
|
+
"path": "LICENSE",
|
|
11
|
+
"blob": "b0a8e9b81083294360c69b4ec45d3d39a2b28197"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
{
|
|
15
|
+
"path": "packages/coding-agent/src/experimental/durable/runtime.ts",
|
|
16
|
+
"blob": "fcd6e5239e999ebf17dea0335fb3dc9cd7f7ce45"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"path": "packages/coding-agent/src/experimental/durable/tui.ts",
|
|
20
|
+
"blob": "397b276cbeb1c90e1fbc96c3cc59e6a25f92dad7"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"path": "packages/coding-agent/src/experimental/durable/harness-setup.ts",
|
|
24
|
+
"blob": "4d6a4be5333ebb8e0f17eac1987f18ea35a91fc7"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"path": "packages/coding-agent/src/experimental/durable/prompt.ts",
|
|
28
|
+
"blob": "8e2f5d5273fba58da8b314766956ed62951c3814"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"path": "packages/coding-agent/src/experimental/durable/sessions.ts",
|
|
32
|
+
"blob": "34f9ceeeaeaf874f7478d56115da2e4f726c6a85"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": "packages/coding-agent/src/experimental/durable/subagent.ts",
|
|
36
|
+
"blob": "e767d54e0bf2908e0f4f41c6933b55c00562eb25"
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"retiredOverlay": {
|
|
40
|
+
"name": "runtime-contacts.patch",
|
|
41
|
+
"sha256": "5b1bd9b48d4ff58ad223006d81cc12c4e0b18959cca5cda5ab3ad89f68bbe898"
|
|
42
|
+
},
|
|
43
|
+
"baselineCarrier": {
|
|
44
|
+
"release": "0.35.1",
|
|
45
|
+
"node": "v24.18.1",
|
|
46
|
+
"files": {
|
|
47
|
+
"runtime.js": "bfd88844fae61e8bd32d57b5b986f48056c64eebb5466d9826318a0783ac9005",
|
|
48
|
+
"tui.js": "f61cc68e5d32552a8196785093ae9e2f4a946088fe71582bb05690517db8c5b4",
|
|
49
|
+
"harness-setup.js": "712b630145dc6f5c655a85b01b1e20299c962542e63c5494d2202bb7bf414a15",
|
|
50
|
+
"prompt.js": "ff1ff765fa313e5cb1c3d7537c901c23adb0562ee433bd050fd35eb3d892134c",
|
|
51
|
+
"sessions.js": "1bd1a04ece66f5ab3ca22860c64dba9abc848334d11a27a9afe543f1365c7254",
|
|
52
|
+
"subagent.js": "8a75d934ef87efbb0bb2006a2141f1ea932560851f1718b991ed947d6efdb54c",
|
|
53
|
+
"LICENSE": "0457f5bcec3b3b211605dfb5d1a49042fd638f3686a410fe099c24a25af13c48"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
8
57
|
"carrier": {
|
|
9
|
-
"comment": "files: the six app files bootstrap.mjs needs (main.ts is replaced by the bootstrap)
|
|
58
|
+
"comment": "files: the six app files bootstrap.mjs needs (main.ts is replaced by the bootstrap), each emitted from its owned `source`. distTargets: every pi-coding-agent internal the stripped files import, as paths under the published dist/; the emitter refuses a set that differs and the resolver answers no other.",
|
|
10
59
|
"emitter": {
|
|
11
60
|
"tool": "node:module.stripTypeScriptTypes",
|
|
12
61
|
"mode": "strip",
|
|
13
62
|
"node": "v24.18.1"
|
|
14
63
|
},
|
|
15
64
|
"license": {
|
|
16
|
-
"source": "LICENSE",
|
|
65
|
+
"source": "pi/pi-durable/src/LICENSE",
|
|
17
66
|
"blob": "b0a8e9b81083294360c69b4ec45d3d39a2b28197",
|
|
18
67
|
"sha256": "0457f5bcec3b3b211605dfb5d1a49042fd638f3686a410fe099c24a25af13c48"
|
|
19
68
|
},
|
|
20
69
|
"files": [
|
|
21
70
|
{
|
|
22
71
|
"name": "runtime.js",
|
|
23
|
-
"source": "
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"sha256": "bfd88844fae61e8bd32d57b5b986f48056c64eebb5466d9826318a0783ac9005"
|
|
72
|
+
"source": "pi/pi-durable/src/experimental/durable/runtime.ts",
|
|
73
|
+
"blob": "36c352291620ab86184172a1b4a0df4562ab9581",
|
|
74
|
+
"sha256": "60c2965eadbdbbe78fce1b7eeb89d97eb3104a30b7d58ebca87009d2e9118403"
|
|
27
75
|
},
|
|
28
76
|
{
|
|
29
77
|
"name": "tui.js",
|
|
30
|
-
"source": "
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"sha256": "f61cc68e5d32552a8196785093ae9e2f4a946088fe71582bb05690517db8c5b4"
|
|
78
|
+
"source": "pi/pi-durable/src/experimental/durable/tui.ts",
|
|
79
|
+
"blob": "dab99cd4778ae1db7f062e64db2d0c068f7e342d",
|
|
80
|
+
"sha256": "0f9ebaf8fbb2a532ccd1a7198e1a25533a99720b5fedbf03f42d5c381db3abc5"
|
|
34
81
|
},
|
|
35
82
|
{
|
|
36
83
|
"name": "harness-setup.js",
|
|
37
|
-
"source": "
|
|
38
|
-
"patched": false,
|
|
84
|
+
"source": "pi/pi-durable/src/experimental/durable/harness-setup.ts",
|
|
39
85
|
"blob": "4d6a4be5333ebb8e0f17eac1987f18ea35a91fc7",
|
|
40
86
|
"sha256": "712b630145dc6f5c655a85b01b1e20299c962542e63c5494d2202bb7bf414a15"
|
|
41
87
|
},
|
|
42
88
|
{
|
|
43
89
|
"name": "prompt.js",
|
|
44
|
-
"source": "
|
|
45
|
-
"patched": false,
|
|
90
|
+
"source": "pi/pi-durable/src/experimental/durable/prompt.ts",
|
|
46
91
|
"blob": "8e2f5d5273fba58da8b314766956ed62951c3814",
|
|
47
92
|
"sha256": "ff1ff765fa313e5cb1c3d7537c901c23adb0562ee433bd050fd35eb3d892134c"
|
|
48
93
|
},
|
|
49
94
|
{
|
|
50
95
|
"name": "sessions.js",
|
|
51
|
-
"source": "
|
|
52
|
-
"patched": false,
|
|
96
|
+
"source": "pi/pi-durable/src/experimental/durable/sessions.ts",
|
|
53
97
|
"blob": "34f9ceeeaeaf874f7478d56115da2e4f726c6a85",
|
|
54
98
|
"sha256": "1bd1a04ece66f5ab3ca22860c64dba9abc848334d11a27a9afe543f1365c7254"
|
|
55
99
|
},
|
|
56
100
|
{
|
|
57
101
|
"name": "subagent.js",
|
|
58
|
-
"source": "
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"sha256": "8a75d934ef87efbb0bb2006a2141f1ea932560851f1718b991ed947d6efdb54c"
|
|
102
|
+
"source": "pi/pi-durable/src/experimental/durable/subagent.ts",
|
|
103
|
+
"blob": "3fa9aebbe93e82cfff436e7b9d4c9877e0ed0e27",
|
|
104
|
+
"sha256": "1cb2390b673a0d914117bc07708c39b004fb10e48b31436bf9d9d2b953a64220"
|
|
62
105
|
}
|
|
63
106
|
],
|
|
64
107
|
"distTargets": [
|
|
@@ -89,17 +132,17 @@
|
|
|
89
132
|
]
|
|
90
133
|
},
|
|
91
134
|
"sdk": {
|
|
92
|
-
"comment": "The published library set the carrier runs against, derived from the 1.
|
|
135
|
+
"comment": "The published library set the carrier runs against, derived from the 1.2.0 declarations. members: the @earendil-works packages the carrier imports and every @earendil-works package those declare. direct: a third-party package a carrier file imports itself (sessions.ts: proper-lockfile, at the version pi-coding-agent 1.2.0 pins). Each is an exact production dependency in package.json; the members' own edges are caret ranges, so the installed physical edges, not semver, are what must equal these versions.",
|
|
93
136
|
"members": {
|
|
94
|
-
"@earendil-works/chord": "1.
|
|
95
|
-
"@earendil-works/pi-agent-core": "1.
|
|
96
|
-
"@earendil-works/pi-ai": "1.
|
|
97
|
-
"@earendil-works/pi-codemode": "1.
|
|
98
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
99
|
-
"@earendil-works/pi-durable": "1.
|
|
100
|
-
"@earendil-works/pi-mcp": "1.
|
|
101
|
-
"@earendil-works/pi-telemetry": "1.
|
|
102
|
-
"@earendil-works/pi-tui": "1.
|
|
137
|
+
"@earendil-works/chord": "1.2.0",
|
|
138
|
+
"@earendil-works/pi-agent-core": "1.2.0",
|
|
139
|
+
"@earendil-works/pi-ai": "1.2.0",
|
|
140
|
+
"@earendil-works/pi-codemode": "1.2.0",
|
|
141
|
+
"@earendil-works/pi-coding-agent": "1.2.0",
|
|
142
|
+
"@earendil-works/pi-durable": "1.2.0",
|
|
143
|
+
"@earendil-works/pi-mcp": "1.2.0",
|
|
144
|
+
"@earendil-works/pi-telemetry": "1.2.0",
|
|
145
|
+
"@earendil-works/pi-tui": "1.2.0"
|
|
103
146
|
},
|
|
104
147
|
"direct": {
|
|
105
148
|
"proper-lockfile": "4.1.2"
|
|
@@ -624,11 +624,12 @@ export async function codexFreshPreflight(
|
|
|
624
624
|
if (config === null || trustReceiptMissing(config, key)) return "codex-birth-trust-missing";
|
|
625
625
|
if (mcpMissing(config)) return "codex-mcp-hand-missing";
|
|
626
626
|
if (visibleIdentityMissing(config)) return "codex-visible-identity-missing";
|
|
627
|
-
const
|
|
628
|
-
const socket = (deps.checkSocket ?? checkCodexSocketFile)(socketPath);
|
|
627
|
+
const socket = (deps.checkSocket ?? checkCodexSocketFile)(resolveCodexDefaultSocketPath(env));
|
|
629
628
|
if (!socket.ok) return "codex-app-server-unavailable";
|
|
629
|
+
// The protocol exchange goes to the endpoint the check verified, never the advertised path
|
|
630
|
+
// again (on Codex 0.162.1 that path is an alias; see `checkCodexSocketFile`).
|
|
630
631
|
const live = await appServerIsLive(
|
|
631
|
-
|
|
632
|
+
socket.connectPath,
|
|
632
633
|
deps.openProtocol ?? realCodexProtocolOpener,
|
|
633
634
|
deps.appServerTimeoutMs ?? DEFAULT_APP_SERVER_TIMEOUT_MS,
|
|
634
635
|
);
|
|
@@ -23,30 +23,135 @@ export function resolveCodexDefaultSocketPath(env: CodexSocketEnv): string {
|
|
|
23
23
|
return path.join(resolveCodexHome(env), "app-server-control", "app-server-control.sock");
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* What the advertised control-socket path is, and the one endpoint a caller may connect to.
|
|
28
|
+
*
|
|
29
|
+
* `connectPath` is a transport endpoint for one exchange: the socket this check verified. It is
|
|
30
|
+
* not an address, a record field or a replacement for `resolveCodexDefaultSocketPath`. `dead`
|
|
31
|
+
* names which absence it is, because the managed launcher treats them differently: an absent
|
|
32
|
+
* path is a first launch, and a dangling rendezvous alias is the leftover of a crashed server
|
|
33
|
+
* that the vendor removes on its next start.
|
|
34
|
+
*/
|
|
35
|
+
export type CodexSocketFileCheck =
|
|
36
|
+
| { ok: true; connectPath: string }
|
|
37
|
+
| { ok: false; status: "dead"; layout: "absent" | "dangling-alias"; reason: string }
|
|
38
|
+
| { ok: false; status: "indeterminate"; reason: string };
|
|
39
|
+
|
|
40
|
+
/** Test seam only: the gate points these at a sandbox. Production callers pass nothing. */
|
|
41
|
+
export interface CodexSocketFileDeps {
|
|
42
|
+
readonly daemonRoot?: string;
|
|
43
|
+
readonly euid?: number;
|
|
44
|
+
}
|
|
27
45
|
|
|
28
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The advertised path is exactly one of two layouts, and anything else is refused.
|
|
48
|
+
*
|
|
49
|
+
* (A) A plain socket owned by the effective uid: Codex up to 0.153.4 binds the advertised
|
|
50
|
+
* path itself. `connectPath` is that path.
|
|
51
|
+
* (B) Codex's rendezvous alias. From 0.162.1 the advertised path "is only an alias": the
|
|
52
|
+
* server binds a 0600 socket under `realpath(/tmp)/codex-daemon-<euid>/` named by the
|
|
53
|
+
* sha256 of the canonical advertised path, then symlinks the advertised path to it
|
|
54
|
+
* (`[source]` rust-v0.162.1 `app-server-transport/src/transport/unix_socket.rs:43-131,
|
|
55
|
+
* 290-304`; `uds/src/daemon_directory.rs:1-38`). Upstream's reason is a privileged
|
|
56
|
+
* rendezvous root that sandboxes hide; the alias also keeps an over-long advertised path
|
|
57
|
+
* usable (`uds/src/lib.rs:167-174`). The alias is accepted only when every hop is the
|
|
58
|
+
* vendor's own: the link is owned by the effective uid; its target is BYTE-EQUAL to the
|
|
59
|
+
* path Codex derives (a relative, foreign, re-hashed or chained target is refused); the
|
|
60
|
+
* daemon directory is a real directory owned by the effective uid with `mode & 0o777`
|
|
61
|
+
* exactly 0700, the vendor's own rule; and the target is a socket owned by the effective
|
|
62
|
+
* uid that no group or other user can open. `connectPath` is the verified target, so the
|
|
63
|
+
* alias is never re-resolved between this check and the connect. A same-euid process can
|
|
64
|
+
* still replace the verified socket file itself; that residual is named, not locked.
|
|
65
|
+
*
|
|
66
|
+
* Only `lstat` and one `readlink` touch the alias; it is never followed or `realpath`ed.
|
|
67
|
+
* The effective uid is the vendor's (`libc::geteuid`), not the real uid.
|
|
68
|
+
*/
|
|
69
|
+
export function checkCodexSocketFile(socketPath: string, deps: CodexSocketFileDeps = {}): CodexSocketFileCheck {
|
|
70
|
+
const euid = deps.euid ?? process.geteuid?.();
|
|
71
|
+
let stat: fs.Stats;
|
|
29
72
|
try {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return { ok: false, status: "indeterminate", reason: `app-server path is not a socket: ${socketPath}` };
|
|
73
|
+
stat = fs.lstatSync(socketPath);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
76
|
+
if (code === "ENOENT") {
|
|
77
|
+
return { ok: false, status: "dead", layout: "absent", reason: `app-server socket is absent: ${socketPath}` };
|
|
36
78
|
}
|
|
37
|
-
|
|
79
|
+
return { ok: false, status: "indeterminate", reason: error instanceof Error ? error.message : String(error) };
|
|
80
|
+
}
|
|
81
|
+
if (stat.isSymbolicLink()) return checkCodexRendezvousAlias(socketPath, stat, euid, deps);
|
|
82
|
+
if (!stat.isSocket()) {
|
|
83
|
+
return { ok: false, status: "indeterminate", reason: `app-server path is not a socket: ${socketPath}` };
|
|
84
|
+
}
|
|
85
|
+
if (euid !== undefined && stat.uid !== euid) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
status: "indeterminate",
|
|
89
|
+
reason: `app-server socket is owned by uid ${stat.uid}, not the effective uid ${euid}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { ok: true, connectPath: socketPath };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function checkCodexRendezvousAlias(
|
|
96
|
+
socketPath: string,
|
|
97
|
+
link: fs.Stats,
|
|
98
|
+
euid: number | undefined,
|
|
99
|
+
deps: CodexSocketFileDeps,
|
|
100
|
+
): CodexSocketFileCheck {
|
|
101
|
+
const refuse = (reason: string): CodexSocketFileCheck => ({ ok: false, status: "indeterminate", reason });
|
|
102
|
+
if (euid === undefined) {
|
|
103
|
+
return refuse(`app-server socket is a symlink and this platform has no effective uid to verify it: ${socketPath}`);
|
|
104
|
+
}
|
|
105
|
+
if (link.uid !== euid) {
|
|
106
|
+
return refuse(`app-server socket symlink is owned by uid ${link.uid}, not the effective uid ${euid}`);
|
|
107
|
+
}
|
|
108
|
+
let daemonRoot: string;
|
|
109
|
+
let expected: string;
|
|
110
|
+
let target: string;
|
|
111
|
+
try {
|
|
112
|
+
daemonRoot = deps.daemonRoot ?? path.join(fs.realpathSync("/tmp"), `codex-daemon-${euid}`);
|
|
113
|
+
const canonical = path.join(fs.realpathSync(path.dirname(socketPath)), path.basename(socketPath));
|
|
114
|
+
expected = path.join(daemonRoot, crypto.createHash("sha256").update(canonical).digest("hex"));
|
|
115
|
+
target = fs.readlinkSync(socketPath);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
return refuse(
|
|
118
|
+
`could not verify the app-server socket symlink ${socketPath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (target !== expected) {
|
|
122
|
+
return refuse(`app-server socket is a symlink that is not Codex's rendezvous alias: ${socketPath} -> ${target}`);
|
|
123
|
+
}
|
|
124
|
+
let directory: fs.Stats;
|
|
125
|
+
try {
|
|
126
|
+
directory = fs.lstatSync(daemonRoot);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return refuse(`Codex's socket directory cannot be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
129
|
+
}
|
|
130
|
+
if (!directory.isDirectory() || directory.uid !== euid || (directory.mode & 0o777) !== 0o700) {
|
|
131
|
+
return refuse(
|
|
132
|
+
`Codex's socket directory ${daemonRoot} must be a real directory owned by uid ${euid} with mode 0700`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
let physical: fs.Stats;
|
|
136
|
+
try {
|
|
137
|
+
physical = fs.lstatSync(expected);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
38
140
|
return {
|
|
39
141
|
ok: false,
|
|
40
|
-
status: "
|
|
41
|
-
|
|
142
|
+
status: "dead",
|
|
143
|
+
layout: "dangling-alias",
|
|
144
|
+
reason: `app-server socket is a dangling rendezvous alias: ${socketPath} -> ${expected}`,
|
|
42
145
|
};
|
|
43
146
|
}
|
|
44
|
-
return
|
|
45
|
-
} catch (error) {
|
|
46
|
-
const code = (error as NodeJS.ErrnoException).code;
|
|
47
|
-
if (code === "ENOENT") return { ok: false, status: "dead", reason: `app-server socket is absent: ${socketPath}` };
|
|
48
|
-
return { ok: false, status: "indeterminate", reason: error instanceof Error ? error.message : String(error) };
|
|
147
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
49
148
|
}
|
|
149
|
+
if (!physical.isSocket() || physical.uid !== euid || (physical.mode & 0o077) !== 0) {
|
|
150
|
+
return refuse(
|
|
151
|
+
`Codex's physical socket ${expected} must be a socket owned by uid ${euid} that no group or other user can open`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return { ok: true, connectPath: expected };
|
|
50
155
|
}
|
|
51
156
|
|
|
52
157
|
export interface CodexRpcProtocol {
|
|
@@ -315,6 +420,7 @@ async function initialize(protocol: CodexRpcProtocol): Promise<void> {
|
|
|
315
420
|
|
|
316
421
|
export interface CodexRoute {
|
|
317
422
|
readonly backend: "codex";
|
|
423
|
+
/** The endpoint `checkCodexSocketFile` verified for this probe, which the send also uses. Not an address. */
|
|
318
424
|
readonly socketPath: string;
|
|
319
425
|
}
|
|
320
426
|
|
|
@@ -359,9 +465,14 @@ export async function probeCodexThread(
|
|
|
359
465
|
nativeSessionId: string,
|
|
360
466
|
deps: CodexAdapterDeps = {},
|
|
361
467
|
): Promise<CodexProbeResult> {
|
|
362
|
-
const
|
|
363
|
-
|
|
468
|
+
const socketCheck = (deps.checkSocket ?? checkCodexSocketFile)(
|
|
469
|
+
resolveCodexDefaultSocketPath(deps.env ?? process.env),
|
|
470
|
+
);
|
|
364
471
|
if (!socketCheck.ok) return { status: socketCheck.status, reason: socketCheck.reason };
|
|
472
|
+
// The probe, and the queue send its route carries, both use the endpoint just verified. The
|
|
473
|
+
// advertised path is not resolved again: on 0.162.1 it is an alias that could be retargeted
|
|
474
|
+
// between the check and a second transport.
|
|
475
|
+
const socketPath = socketCheck.connectPath;
|
|
365
476
|
let protocol: CodexRpcProtocol;
|
|
366
477
|
try {
|
|
367
478
|
protocol = await (deps.openProtocol ?? realCodexProtocolOpener).open(socketPath);
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* under (#129 W, #130).
|
|
4
4
|
*
|
|
5
5
|
* The experimental durable app is source-only upstream: published `@earendil-works/pi-coding-agent`
|
|
6
|
-
* ships no `dist/experimental`. Entwurf therefore ships the app itself as a CARRIER —
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* ships no `dist/experimental`. Entwurf therefore ships the app itself as a CARRIER — its owned
|
|
7
|
+
* source (pi/pi-durable/src, #141), type-stripped and relocated (pi/pi-durable/carrier/, emitted only
|
|
8
|
+
* by scripts/emit-pi-durable-carrier.ts) — and declares the published SDK set it runs
|
|
9
9
|
* against as exact production dependencies. Nothing is provisioned beside the package: there is no
|
|
10
10
|
* runtime directory, no source checkout and no model data to copy (it rides the published pi-ai).
|
|
11
11
|
*
|
|
@@ -47,7 +47,7 @@ export type PiDurableRuntimeRejectReason =
|
|
|
47
47
|
export interface PiDurablePackageLayout {
|
|
48
48
|
readonly root: string;
|
|
49
49
|
readonly overlayDir: string;
|
|
50
|
-
/** The emitted
|
|
50
|
+
/** The app files emitted from the owned source, and their license. */
|
|
51
51
|
readonly carrierDir: string;
|
|
52
52
|
/** The `--import` resolver the launcher runs the bootstrap under. */
|
|
53
53
|
readonly resolver: string;
|