@junghanacs/entwurf 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +240 -0
- package/BASELINE.md +227 -0
- package/CHANGELOG.md +1210 -0
- package/CONTRIBUTING.md +63 -0
- package/DELIVERY.md +209 -0
- package/LICENSE +21 -0
- package/README.md +504 -0
- package/VERIFY.md +260 -0
- package/demo/README.md +188 -0
- package/demo/demo-baseline.sh +156 -0
- package/demo/demo.sh +183 -0
- package/docs/assets/entwurf-demo.gif +0 -0
- package/docs/assets/entwurf-doomemacs.gif +0 -0
- package/docs/assets/entwurf-entwurf.gif +0 -0
- package/docs/assets/entwurf-hero.jpg +0 -0
- package/docs/setup-clean-host.md +305 -0
- package/mcp/entwurf-bridge/src/index.ts +513 -0
- package/mcp/entwurf-bridge/start.sh +25 -0
- package/mcp/entwurf-bridge/test.sh +54 -0
- package/mcp/tsconfig.json +29 -0
- package/package.json +130 -0
- package/pi/entwurf-capabilities.json +9 -0
- package/pi/entwurf-targets.json +20 -0
- package/pi/meta-bridge/.claude-plugin/marketplace.json +12 -0
- package/pi/meta-bridge/entwurf-meta-receive/.claude-plugin/plugin.json +5 -0
- package/pi/meta-bridge/entwurf-meta-receive/hooks/hooks.json +50 -0
- package/pi/meta-bridge/entwurf-meta-receive/scripts/doorbell.sh +54 -0
- package/pi/settings.reference.json +43 -0
- package/pi/skill-plugin-example/.claude-plugin/plugin.json +4 -0
- package/pi/skill-plugin-example/skills/hello/SKILL.md +18 -0
- package/pi-extensions/acp-provider.ts +63 -0
- package/pi-extensions/entwurf-control.ts +1692 -0
- package/pi-extensions/lib/acp/acp-client.ts +90 -0
- package/pi-extensions/lib/acp/augment.ts +238 -0
- package/pi-extensions/lib/acp/backend-adapter.ts +319 -0
- package/pi-extensions/lib/acp/backend.ts +873 -0
- package/pi-extensions/lib/acp/config.ts +552 -0
- package/pi-extensions/lib/acp/context.ts +177 -0
- package/pi-extensions/lib/acp/engraving.ts +123 -0
- package/pi-extensions/lib/acp/event-mapper.ts +339 -0
- package/pi-extensions/lib/acp/models.ts +102 -0
- package/pi-extensions/lib/acp/overlay.ts +220 -0
- package/pi-extensions/lib/acp/prompts/engraving.md +1 -0
- package/pi-extensions/lib/acp/session-store.ts +418 -0
- package/pi-extensions/lib/acp/tool-surface.ts +184 -0
- package/pi-extensions/lib/entwurf-control-rpc.ts +208 -0
- package/pi-extensions/lib/entwurf-core.ts +2033 -0
- package/pi-extensions/lib/entwurf-deliverability.ts +123 -0
- package/pi-extensions/lib/entwurf-fact-provider.ts +158 -0
- package/pi-extensions/lib/entwurf-facts.ts +251 -0
- package/pi-extensions/lib/entwurf-mailbox-guard.ts +100 -0
- package/pi-extensions/lib/entwurf-peers-render.ts +154 -0
- package/pi-extensions/lib/entwurf-preflight.ts +247 -0
- package/pi-extensions/lib/entwurf-resume-args.ts +88 -0
- package/pi-extensions/lib/entwurf-self-address.ts +117 -0
- package/pi-extensions/lib/entwurf-v2-contract.ts +427 -0
- package/pi-extensions/lib/entwurf-v2-decider.ts +441 -0
- package/pi-extensions/lib/entwurf-v2-lock.ts +412 -0
- package/pi-extensions/lib/entwurf-v2-mailbox.ts +87 -0
- package/pi-extensions/lib/entwurf-v2-production.ts +326 -0
- package/pi-extensions/lib/entwurf-v2-release.ts +149 -0
- package/pi-extensions/lib/entwurf-v2-resume-marker.ts +34 -0
- package/pi-extensions/lib/entwurf-v2-runner.ts +196 -0
- package/pi-extensions/lib/entwurf-v2-send-fallback.ts +166 -0
- package/pi-extensions/lib/entwurf-v2-send.ts +271 -0
- package/pi-extensions/lib/entwurf-v2-spawn-production.ts +337 -0
- package/pi-extensions/lib/entwurf-v2-spawn.ts +323 -0
- package/pi-extensions/lib/entwurf-v2-surface.ts +205 -0
- package/pi-extensions/lib/meta-mailbox-body.ts +80 -0
- package/pi-extensions/lib/meta-session.ts +1951 -0
- package/pi-extensions/lib/project-trust-handler.ts +154 -0
- package/pi-extensions/lib/session-id.js +57 -0
- package/pi-extensions/lib/socket-discovery.ts +346 -0
- package/pi-extensions/lib/socket-probe.ts +92 -0
- package/pi-extensions/meta-bridge-hook.ts +250 -0
- package/pi-extensions/model-lock.ts +235 -0
- package/prompts/engraving.md +27 -0
- package/protocol.js +31 -0
- package/run.sh +2832 -0
- package/scripts/check-acp-backend-preflight.ts +130 -0
- package/scripts/check-acp-carrier-augment.ts +297 -0
- package/scripts/check-acp-config.ts +322 -0
- package/scripts/check-acp-event-mapper.ts +253 -0
- package/scripts/check-acp-overlay.ts +154 -0
- package/scripts/check-acp-prompt-builder.ts +191 -0
- package/scripts/check-acp-provider-surface.ts +159 -0
- package/scripts/check-acp-sdk-surface.ts +211 -0
- package/scripts/check-acp-session-reuse.ts +756 -0
- package/scripts/check-acp-session-store.ts +387 -0
- package/scripts/check-acp-tool-surface.ts +159 -0
- package/scripts/check-entwurf-bridge-boot.ts +199 -0
- package/scripts/check-entwurf-capabilities.ts +123 -0
- package/scripts/check-entwurf-control-rpc.ts +187 -0
- package/scripts/check-entwurf-deliverability.ts +158 -0
- package/scripts/check-entwurf-fact-provider.ts +316 -0
- package/scripts/check-entwurf-facts.ts +358 -0
- package/scripts/check-entwurf-mailbox-guard.ts +264 -0
- package/scripts/check-entwurf-peers-surface.ts +235 -0
- package/scripts/check-entwurf-resume-args.ts +149 -0
- package/scripts/check-entwurf-self-address.ts +209 -0
- package/scripts/check-entwurf-session-identity.ts +703 -0
- package/scripts/check-entwurf-v2-contract.ts +536 -0
- package/scripts/check-entwurf-v2-decider.ts +795 -0
- package/scripts/check-entwurf-v2-lock.ts +368 -0
- package/scripts/check-entwurf-v2-mailbox.ts +228 -0
- package/scripts/check-entwurf-v2-matrix.ts +437 -0
- package/scripts/check-entwurf-v2-production.ts +406 -0
- package/scripts/check-entwurf-v2-release.ts +245 -0
- package/scripts/check-entwurf-v2-runner.ts +452 -0
- package/scripts/check-entwurf-v2-send-fallback.ts +371 -0
- package/scripts/check-entwurf-v2-send.ts +454 -0
- package/scripts/check-entwurf-v2-spawn-production.ts +452 -0
- package/scripts/check-entwurf-v2-spawn.ts +398 -0
- package/scripts/check-entwurf-v2-surface.ts +352 -0
- package/scripts/check-keyset-overlap.py +120 -0
- package/scripts/check-mailbox-receipt-state.ts +170 -0
- package/scripts/check-meta-capability-source.ts +112 -0
- package/scripts/check-meta-dual-consumers.ts +154 -0
- package/scripts/check-meta-dual-read.ts +158 -0
- package/scripts/check-meta-listing.ts +138 -0
- package/scripts/check-meta-mailbox-state-write.ts +135 -0
- package/scripts/check-meta-migration.ts +212 -0
- package/scripts/check-meta-receiver-marker.ts +185 -0
- package/scripts/check-meta-record-v2.ts +191 -0
- package/scripts/check-meta-session.ts +673 -0
- package/scripts/check-model-lock.ts +408 -0
- package/scripts/check-package-source-routing.ts +253 -0
- package/scripts/check-pi-preflight.ts +304 -0
- package/scripts/check-project-trust-handler.ts +265 -0
- package/scripts/check-shell-quote.ts +121 -0
- package/scripts/check-socket-discovery.ts +428 -0
- package/scripts/check-socket-probe.ts +106 -0
- package/scripts/fixtures/probe-mcp-server.ts +33 -0
- package/scripts/gnew-rpc-drive.ts +211 -0
- package/scripts/lib/acp-child-cleanup.ts +116 -0
- package/scripts/meta-bridge-doctor.sh +315 -0
- package/scripts/meta-bridge-hook-log.sh +26 -0
- package/scripts/meta-bridge-install.sh +135 -0
- package/scripts/meta-bridge-prune.ts +199 -0
- package/scripts/meta-bridge-state.py +549 -0
- package/scripts/meta-bridge-statusline.sh +192 -0
- package/scripts/meta-bridge-store-doctor.ts +64 -0
- package/scripts/meta-bridge-uninstall.sh +39 -0
- package/scripts/new-session-id.ts +25 -0
- package/scripts/postinstall-chmod.cjs +58 -0
- package/scripts/raw-async-delivery/README.md +258 -0
- package/scripts/raw-async-delivery/cc-enqueue-addressed.sh +35 -0
- package/scripts/raw-async-delivery/cc-mailbox-rewake.sh +38 -0
- package/scripts/raw-async-delivery/cc-watch-filechanged.sh +19 -0
- package/scripts/raw-async-delivery/cc-watch-sessionstart.sh +15 -0
- package/scripts/raw-async-delivery/codex-local-appserver.sh +39 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/.claude-plugin/plugin.json +5 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/hooks/hooks.json +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-filechanged.sh +28 -0
- package/scripts/raw-async-delivery/plugin-entwurf-receive/scripts/watch-sessionstart.sh +27 -0
- package/scripts/raw-async-delivery/raw-agy-send.sh +29 -0
- package/scripts/raw-async-delivery/raw-claude-enqueue.sh +30 -0
- package/scripts/raw-async-delivery/raw-codex-ws-turn-start.py +164 -0
- package/scripts/raw-async-delivery/repro-addressed-routing.sh +96 -0
- package/scripts/raw-async-delivery/repro-plugin-idle-wake.sh +104 -0
- package/scripts/resolve-acp-bridge.ts +25 -0
- package/scripts/smoke-acp-bundled-mcp-live.ts +292 -0
- package/scripts/smoke-acp-carrier-augment-live.ts +192 -0
- package/scripts/smoke-acp-mcp-live.ts +129 -0
- package/scripts/smoke-acp-memory-containment-live.ts +389 -0
- package/scripts/smoke-acp-overlay-live.ts +314 -0
- package/scripts/smoke-acp-provider-live.ts +162 -0
- package/scripts/smoke-acp-raw-turn-live.ts +261 -0
- package/scripts/smoke-acp-session-reuse-live.ts +172 -0
- package/scripts/smoke-acp-skill-live.ts +144 -0
- package/scripts/smoke-acp-socket-citizen-live.ts +168 -0
- package/scripts/smoke-claude-native-resume-live.sh +198 -0
- package/scripts/smoke-entwurf-v2-matrix-live.ts +398 -0
- package/scripts/smoke-entwurf-v2-spawn-live.ts +175 -0
- package/scripts/smoke-entwurf-v2-spawn-resume-live.ts +428 -0
- package/scripts/smoke-meta-async-drift.sh +171 -0
- package/scripts/smoke-meta-honesty.sh +147 -0
- package/scripts/smoke-meta-install-state.sh +403 -0
- package/scripts/smoke-meta-keyset-guard.sh +111 -0
- package/scripts/smoke-meta-prune.sh +174 -0
- package/scripts/smoke-resident-garden-guard.sh +433 -0
- package/scripts/smoke-session-id-name.ts +187 -0
- package/scripts/tsconfig.json +34 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entwurf-v2-production — 5d-2b: the ctx-free PRODUCTION assembly of `runEntwurfV2`'s deps.
|
|
3
|
+
* `makeProductionEntwurfV2Deps(opts)` wires the real 5b decider IO seams + the three 5c
|
|
4
|
+
* transport hands into one `EntwurfV2RunDeps` (`{decide, executor}`). It touches NO
|
|
5
|
+
* `ExtensionContext`/`ExtensionAPI` — the wiring SITE (5d-3, entwurf-control.ts) builds the
|
|
6
|
+
* `senderProvider` from `buildLocalSenderEnvelope(ctx)` and passes it in, so this module
|
|
7
|
+
* stays in `lib/` with the rest of the gate-/smoke-testable v2 core.
|
|
8
|
+
*
|
|
9
|
+
* Three invariants this factory exists to guarantee (each gate-proven):
|
|
10
|
+
* - ONE lock domain (Q2/QB3). A single `release` closure bound to `lockDir` is the lock
|
|
11
|
+
* authority for EVERY hand: the decider's `releaseLock`, the control-send hand's
|
|
12
|
+
* `releaseLock`, AND the spawn watcher's `releaseFn`. The spawn factory's default
|
|
13
|
+
* `releaseFn` ignores `lockDir` (it would release into the DEFAULT lock dir) — passing
|
|
14
|
+
* `release` explicitly is what closes that split-brain.
|
|
15
|
+
* - ONE mailbox sender (Q3). A single `makeProductionSendViaMailbox` instance is the
|
|
16
|
+
* top-level meta-mailbox hand AND the control-send dead-fallback's mailbox enqueue, so a
|
|
17
|
+
* direct send and a fallback send never drift in sender envelope / dirs.
|
|
18
|
+
* - ONE path set (Q5). `sessionsDir`/`mailboxDir`/`lockDir`/`controlSocketDir` are resolved
|
|
19
|
+
* ONCE and threaded to the decider, the dead-fallback resolver, and the enqueue, so the
|
|
20
|
+
* plan the decider plants and the plan the fallback plants address the same store.
|
|
21
|
+
*
|
|
22
|
+
* `resolveTarget` (QB1): a MISSING meta-record is a soft `bad-target` (identity:null); a
|
|
23
|
+
* PRESENT-but-corrupt record fails LOUD (the read throws drift) — never matched by string.
|
|
24
|
+
* `preProbeAddressConflict` (QB2): a single record-side `lstat` (NO connect) of the target's
|
|
25
|
+
* canonical socket path; `indeterminate` (EACCES/unknown) fails loud rather than silently
|
|
26
|
+
* claiming "no conflict". The decider's later `inspectSocket` probe is a SEPARATE step.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
type RpcClientOptions,
|
|
31
|
+
type RpcCommand,
|
|
32
|
+
type RpcResponse,
|
|
33
|
+
sendRpcCommand as realSendRpc,
|
|
34
|
+
type SenderEnvelope,
|
|
35
|
+
} from "./entwurf-control-rpc.ts";
|
|
36
|
+
import {
|
|
37
|
+
type MailboxDeliverabilityResult,
|
|
38
|
+
mailboxConversationalDeliverable,
|
|
39
|
+
receiverMarkerMatchesIdentity,
|
|
40
|
+
} from "./entwurf-deliverability.ts";
|
|
41
|
+
import { isNonPiGardenIdSocketConflict } from "./entwurf-facts.ts";
|
|
42
|
+
import { type PreflightInput, type PreflightOutcome, preflight as realPreflight } from "./entwurf-preflight.ts";
|
|
43
|
+
import { isLivenessSupported } from "./entwurf-v2-contract.ts";
|
|
44
|
+
import {
|
|
45
|
+
type DispatchDeciderDeps,
|
|
46
|
+
type DispatchInput,
|
|
47
|
+
decideDispatch,
|
|
48
|
+
type TargetResolution,
|
|
49
|
+
} from "./entwurf-v2-decider.ts";
|
|
50
|
+
import {
|
|
51
|
+
type AcquireLockResult,
|
|
52
|
+
ENTWURF_V2_LOCK_DIR,
|
|
53
|
+
type LockClaim,
|
|
54
|
+
acquireLock as realAcquireLock,
|
|
55
|
+
releaseLock as realReleaseLock,
|
|
56
|
+
} from "./entwurf-v2-lock.ts";
|
|
57
|
+
import { makeProductionSendViaMailbox } from "./entwurf-v2-mailbox.ts";
|
|
58
|
+
import type { DispatchExecutorDeps, EntwurfV2RunDeps } from "./entwurf-v2-runner.ts";
|
|
59
|
+
import {
|
|
60
|
+
type ControlSocketPlan,
|
|
61
|
+
type ControlSocketSendDeps,
|
|
62
|
+
executeControlSocketSend,
|
|
63
|
+
type MetaMailboxPlan,
|
|
64
|
+
type RpcSendResult,
|
|
65
|
+
} from "./entwurf-v2-send.ts";
|
|
66
|
+
import { resolveDeadControlSendFallback } from "./entwurf-v2-send-fallback.ts";
|
|
67
|
+
import type { SpawnBgPlan } from "./entwurf-v2-spawn.ts";
|
|
68
|
+
import { executeSpawnBgResume } from "./entwurf-v2-spawn.ts";
|
|
69
|
+
import { makeProductionSpawnBgResumeDeps, type ProductionSpawnOpts } from "./entwurf-v2-spawn-production.ts";
|
|
70
|
+
import {
|
|
71
|
+
defaultMetaMailboxDir,
|
|
72
|
+
defaultMetaSessionsDir,
|
|
73
|
+
type EnqueueMetaMessageOptions,
|
|
74
|
+
type EnqueueMetaMessageResult,
|
|
75
|
+
enqueueMetaMessage,
|
|
76
|
+
type MetaIdentity,
|
|
77
|
+
type MetaReceiverMarker,
|
|
78
|
+
metaCapabilityFor,
|
|
79
|
+
metaRecordExistsByGardenId,
|
|
80
|
+
readMetaIdentityByGardenId,
|
|
81
|
+
readMetaReceiverMarker,
|
|
82
|
+
} from "./meta-session.ts";
|
|
83
|
+
import {
|
|
84
|
+
CONTROL_SOCKET_DIR,
|
|
85
|
+
controlSocketPath,
|
|
86
|
+
inspectControlSocketPath,
|
|
87
|
+
inspectTargetControlSocket,
|
|
88
|
+
isSocketOnlyPiCandidate,
|
|
89
|
+
type TargetSocketInspection,
|
|
90
|
+
} from "./socket-discovery.ts";
|
|
91
|
+
import { classifyConnectError, probeSocketLiveness, type SocketLiveness } from "./socket-probe.ts";
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The leaf IO the production factory calls. Every field defaults to the REAL fn; the 5d-2b
|
|
95
|
+
* gate overrides them with spies to prove the wiring (shared closures, lockDir binding,
|
|
96
|
+
* shared mailbox instance) WITHOUT a real socket/lock/meta-record. This is the only seam —
|
|
97
|
+
* the factory's COMPOSITION (which closure goes to which hand) is never overridable.
|
|
98
|
+
*/
|
|
99
|
+
export interface ProductionEntwurfV2Seams {
|
|
100
|
+
metaRecordExists: (gid: string, sessionsDir: string) => boolean;
|
|
101
|
+
readIdentity: (gid: string, sessionsDir: string) => MetaIdentity;
|
|
102
|
+
/** Read the target's receiver presence marker (null = absent / dead owner / corrupt). The
|
|
103
|
+
* SE-2 2d-3 active-receiver source; the factory's `mailboxDeliverabilityFor` closure verifies
|
|
104
|
+
* its identity match. */
|
|
105
|
+
readReceiverMarker: (gardenId: string) => MetaReceiverMarker | null;
|
|
106
|
+
/** Record-side lstat of the EXACT target socket path (no connect) for the pre-probe conflict. */
|
|
107
|
+
inspectPath: (socketPath: string) => Promise<TargetSocketInspection>;
|
|
108
|
+
acquireLock: (gid: string, deps: { dir?: string }) => AcquireLockResult;
|
|
109
|
+
releaseLock: (claim: LockClaim, deps: { dir?: string }) => unknown;
|
|
110
|
+
inspectSocket: (gid: string, dir: string) => Promise<TargetSocketInspection>;
|
|
111
|
+
probeSocket: (socketPath: string) => Promise<SocketLiveness>;
|
|
112
|
+
preflight: (input: PreflightInput) => PreflightOutcome;
|
|
113
|
+
classifyConnect: (code: string | undefined) => "dead" | "indeterminate";
|
|
114
|
+
sendRpc: (socketPath: string, command: RpcCommand, options?: RpcClientOptions) => Promise<{ response: RpcResponse }>;
|
|
115
|
+
enqueue: (opts: EnqueueMetaMessageOptions) => EnqueueMetaMessageResult;
|
|
116
|
+
/** Extra spawn-factory overrides (timers/spawnChild/probe) for a deterministic spawn gate.
|
|
117
|
+
* `releaseFn` is NOT overridable here — the factory injects the shared `release` (QB3). */
|
|
118
|
+
spawnOverrides: Omit<ProductionSpawnOpts, "releaseFn">;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ProductionEntwurfV2Opts {
|
|
122
|
+
/** Built at the wiring site from `buildLocalSenderEnvelope(ctx)`, decorated with its
|
|
123
|
+
* HONEST pi-session replyability (SE-1 2e-a: `replyable` reflects whether the canonical
|
|
124
|
+
* control socket actually exists, not a hardcoded true). ONE provider feeds the
|
|
125
|
+
* control-socket RPC sender AND the meta-mailbox body sender (they share the envelope). */
|
|
126
|
+
senderProvider: () => SenderEnvelope | undefined;
|
|
127
|
+
/** pi agent dir holding `trust.json` (preflight). Omit → preflight's own default. */
|
|
128
|
+
agentDir?: string;
|
|
129
|
+
/** Operator-policy auto-approve roots (preflight prefix promotion). No package default. */
|
|
130
|
+
prefixRoots?: readonly string[];
|
|
131
|
+
lockDir?: string;
|
|
132
|
+
sessionsDir?: string;
|
|
133
|
+
mailboxDir?: string;
|
|
134
|
+
controlSocketDir?: string;
|
|
135
|
+
observeTimeoutMs?: number;
|
|
136
|
+
killGraceMs?: number;
|
|
137
|
+
/** Gate/smoke seam overrides — defaults are the real IO. */
|
|
138
|
+
seams?: Partial<ProductionEntwurfV2Seams>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Map a record-side socket inspection to the singleton (socketGids, symlinkedGids) the
|
|
142
|
+
* `isNonPiGardenIdSocketConflict` predicate consumes. `indeterminate` fails LOUD (QB2): an
|
|
143
|
+
* unprovable conflict must NOT be folded to "no conflict" — that would silently allow an
|
|
144
|
+
* unsupported-backend mailbox send onto a quarantined address. */
|
|
145
|
+
function conflictSetsFor(
|
|
146
|
+
gid: string,
|
|
147
|
+
inspection: TargetSocketInspection,
|
|
148
|
+
): { socketGids: ReadonlySet<string>; symlinkedGids: ReadonlySet<string> } {
|
|
149
|
+
switch (inspection.kind) {
|
|
150
|
+
case "absent":
|
|
151
|
+
return { socketGids: new Set(), symlinkedGids: new Set() };
|
|
152
|
+
case "socket-file":
|
|
153
|
+
return { socketGids: new Set([gid]), symlinkedGids: new Set() };
|
|
154
|
+
case "address-conflict":
|
|
155
|
+
// symlink → the symlink axis; not-socket → a non-symlink `.sock` entry, same axis
|
|
156
|
+
// the fact-provider's `socketGids` carries (NOT only real sockets).
|
|
157
|
+
return inspection.reason === "symlink"
|
|
158
|
+
? { socketGids: new Set(), symlinkedGids: new Set([gid]) }
|
|
159
|
+
: { socketGids: new Set([gid]), symlinkedGids: new Set() };
|
|
160
|
+
case "indeterminate":
|
|
161
|
+
throw new Error(
|
|
162
|
+
`entwurf-v2-production: cannot resolve target ${gid} — its control socket lstat is indeterminate (${inspection.error}); refusing to claim "no address conflict".`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Assemble the production `runEntwurfV2` deps. See the module header for the three wiring
|
|
169
|
+
* invariants. The returned `{ decide, executor }` is exactly the shape `runEntwurfV2` joins.
|
|
170
|
+
*/
|
|
171
|
+
export function makeProductionEntwurfV2Deps(opts: ProductionEntwurfV2Opts): EntwurfV2RunDeps {
|
|
172
|
+
// ── ONE path set (Q5): resolve every dir ONCE ─────────────────────────────
|
|
173
|
+
const lockDir = opts.lockDir ?? ENTWURF_V2_LOCK_DIR;
|
|
174
|
+
const sessionsDir = opts.sessionsDir ?? defaultMetaSessionsDir();
|
|
175
|
+
const mailboxDir = opts.mailboxDir ?? defaultMetaMailboxDir();
|
|
176
|
+
const controlSocketDir = opts.controlSocketDir ?? CONTROL_SOCKET_DIR;
|
|
177
|
+
|
|
178
|
+
const s = opts.seams ?? {};
|
|
179
|
+
const io: ProductionEntwurfV2Seams = {
|
|
180
|
+
metaRecordExists: s.metaRecordExists ?? metaRecordExistsByGardenId,
|
|
181
|
+
readIdentity: s.readIdentity ?? readMetaIdentityByGardenId,
|
|
182
|
+
readReceiverMarker: s.readReceiverMarker ?? ((gid: string) => readMetaReceiverMarker({ gardenId: gid })),
|
|
183
|
+
inspectPath: s.inspectPath ?? inspectControlSocketPath,
|
|
184
|
+
acquireLock: s.acquireLock ?? realAcquireLock,
|
|
185
|
+
releaseLock: s.releaseLock ?? realReleaseLock,
|
|
186
|
+
inspectSocket: s.inspectSocket ?? inspectTargetControlSocket,
|
|
187
|
+
probeSocket: s.probeSocket ?? probeSocketLiveness,
|
|
188
|
+
preflight: s.preflight ?? realPreflight,
|
|
189
|
+
classifyConnect: s.classifyConnect ?? classifyConnectError,
|
|
190
|
+
sendRpc: s.sendRpc ?? realSendRpc,
|
|
191
|
+
enqueue: s.enqueue ?? enqueueMetaMessage,
|
|
192
|
+
spawnOverrides: s.spawnOverrides ?? {},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// ── ONE lock domain (Q2/QB3): a single lockDir-bound release for ALL hands ─
|
|
196
|
+
const acquire = (gid: string): AcquireLockResult => io.acquireLock(gid, { dir: lockDir });
|
|
197
|
+
const release = (claim: LockClaim): void => {
|
|
198
|
+
io.releaseLock(claim, { dir: lockDir });
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// ── ONE mailbox sender (Q3): one instance for the hand AND the dead-fallback ─
|
|
202
|
+
const sendViaMailbox = makeProductionSendViaMailbox({
|
|
203
|
+
senderProvider: opts.senderProvider,
|
|
204
|
+
enqueue: io.enqueue,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ── ONE deliverability seam (SE-2 2d-3): wake-mode capability AND a live active-
|
|
208
|
+
// receiver (a presence marker that matches THIS identity). The SAME closure is injected
|
|
209
|
+
// into the decider AND the dead-fallback, so a direct send and a re-resolved fallback
|
|
210
|
+
// send can never drift to different deliverability verdicts. recordBacked is true by
|
|
211
|
+
// construction — resolveTarget already proved the record exists before any unsupported-
|
|
212
|
+
// backend mailbox route, and the closure is only consulted on that route. A null /
|
|
213
|
+
// dead-owner / identity-mismatched marker is fail-closed to inactive (SE-2): a reply to a
|
|
214
|
+
// terminated self-fetch citizen is rejected, not enqueued as mailbox garbage. ──────────
|
|
215
|
+
const mailboxDeliverabilityFor = (identity: MetaIdentity): MailboxDeliverabilityResult => {
|
|
216
|
+
const wakeMode = metaCapabilityFor(identity.backend).wakeMode;
|
|
217
|
+
const marker = io.readReceiverMarker(identity.gardenId);
|
|
218
|
+
const matched = receiverMarkerMatchesIdentity(marker, identity);
|
|
219
|
+
return mailboxConversationalDeliverable({
|
|
220
|
+
wakeMode,
|
|
221
|
+
recordBacked: true,
|
|
222
|
+
ownerAlive: matched,
|
|
223
|
+
watchArmed: matched,
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// ── target resolution (QB1 + QB2) ─────────────────────────────────────────
|
|
228
|
+
const resolveTarget = async (gid: string): Promise<TargetResolution> => {
|
|
229
|
+
// MISSING record → not a citizen. But a record-LESS, gid-shaped, NON-SYMLINK control
|
|
230
|
+
// socket means the gid is still an addressable socket-only pi endpoint (A1 narrow,
|
|
231
|
+
// 0.11.0): entwurf_v2 accepts it as a FIRE-AND-FORGET control-send target only. This is
|
|
232
|
+
// PROBE-FREE — a single `inspectPath` lstat (NO connect), the SAME seam the pre-probe
|
|
233
|
+
// conflict uses — and `isSocketOnlyPiCandidate` promotes ONLY a confirmed non-symlink
|
|
234
|
+
// socket (`socket-file`); symlink/absent/not-socket/indeterminate stay a plain
|
|
235
|
+
// bad-target. PRESENT record → read (drift/corrupt throws = fail-loud).
|
|
236
|
+
if (!io.metaRecordExists(gid, sessionsDir)) {
|
|
237
|
+
const inspection = await io.inspectPath(controlSocketPath(gid, controlSocketDir));
|
|
238
|
+
return { identity: null, preProbeAddressConflict: false, socketOnlyPi: isSocketOnlyPiCandidate(inspection) };
|
|
239
|
+
}
|
|
240
|
+
const identity = io.readIdentity(gid, sessionsDir);
|
|
241
|
+
// `preProbeAddressConflict` is the record-side NON-PI conflict ONLY (B1). An in-domain
|
|
242
|
+
// (pi) target's socket lstat/connect MUST run UNDER the lock in the decider's later
|
|
243
|
+
// `inspectSocket` step (1C: lock BEFORE lstat/connect) — so a pi target short-circuits
|
|
244
|
+
// here with NO pre-lock lstat. Doing the lstat here would (a) be a needless pre-lock IO
|
|
245
|
+
// and (b) turn a pi target's `indeterminate` lstat into a top-level throw, stealing it
|
|
246
|
+
// from the under-lock `inspectSocket → indeterminate → indeterminate-no-spawn` path.
|
|
247
|
+
if (isLivenessSupported(identity.backend)) {
|
|
248
|
+
return { identity, preProbeAddressConflict: false };
|
|
249
|
+
}
|
|
250
|
+
// Only an unsupported (non-pi) citizen reaches the record-side lstat: a single lstat (no
|
|
251
|
+
// connect) of the canonical path; `indeterminate` fails loud (QB2 — never "no conflict").
|
|
252
|
+
const inspection = await io.inspectPath(controlSocketPath(gid, controlSocketDir));
|
|
253
|
+
const { socketGids, symlinkedGids } = conflictSetsFor(gid, inspection);
|
|
254
|
+
const preProbeAddressConflict = isNonPiGardenIdSocketConflict(identity.backend, gid, socketGids, symlinkedGids);
|
|
255
|
+
return { identity, preProbeAddressConflict };
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const inspectSocket = (gid: string): Promise<TargetSocketInspection> => io.inspectSocket(gid, controlSocketDir);
|
|
259
|
+
const probeSocket = (socketPath: string): Promise<SocketLiveness> => io.probeSocket(socketPath);
|
|
260
|
+
|
|
261
|
+
// ── decider deps (5b) ─────────────────────────────────────────────────────
|
|
262
|
+
const deciderDeps: DispatchDeciderDeps = {
|
|
263
|
+
resolveTarget,
|
|
264
|
+
acquireLock: acquire,
|
|
265
|
+
releaseLock: release,
|
|
266
|
+
inspectSocket,
|
|
267
|
+
probeSocket,
|
|
268
|
+
preflightForCwd: (cwd: string): PreflightOutcome =>
|
|
269
|
+
io.preflight({ cwd, agentDir: opts.agentDir, prefixRoots: opts.prefixRoots }),
|
|
270
|
+
mailboxDeliverabilityFor,
|
|
271
|
+
mailboxDir,
|
|
272
|
+
sessionsDir,
|
|
273
|
+
observeTimeoutMs: opts.observeTimeoutMs,
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
// ── control-send hand deps (5c-2): the dead-fallback shares resolveTarget /
|
|
277
|
+
// inspect / probe / dirs with the decider; the mailbox enqueue is the SAME
|
|
278
|
+
// `sendViaMailbox` instance; the release is the SAME `release` closure. ─────
|
|
279
|
+
const controlSendDeps: ControlSocketSendDeps = {
|
|
280
|
+
sendOverSocket: async (plan: ControlSocketPlan): Promise<RpcSendResult> => {
|
|
281
|
+
const { response } = await io.sendRpc(plan.socketPath, {
|
|
282
|
+
type: "send",
|
|
283
|
+
message: plan.message,
|
|
284
|
+
mode: plan.mode,
|
|
285
|
+
wants_reply: plan.wantsReply,
|
|
286
|
+
sender: opts.senderProvider(),
|
|
287
|
+
});
|
|
288
|
+
return { success: response.success, error: response.error };
|
|
289
|
+
},
|
|
290
|
+
classifyConnect: io.classifyConnect,
|
|
291
|
+
releaseLock: release,
|
|
292
|
+
deadFallback: (plan: ControlSocketPlan, lock: LockClaim) =>
|
|
293
|
+
resolveDeadControlSendFallback(plan, lock, {
|
|
294
|
+
resolveTarget,
|
|
295
|
+
inspectSocket,
|
|
296
|
+
probeSocket,
|
|
297
|
+
mailboxDeliverabilityFor,
|
|
298
|
+
mailboxDir,
|
|
299
|
+
sessionsDir,
|
|
300
|
+
}),
|
|
301
|
+
sendViaMailbox,
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// ── executor: the three transport hands, each pre-bound ───────────────────
|
|
305
|
+
const executor: DispatchExecutorDeps = {
|
|
306
|
+
sendControl: (plan, lock) => executeControlSocketSend(plan as ControlSocketPlan, lock, controlSendDeps),
|
|
307
|
+
resumeSpawnBg: (plan, lock) =>
|
|
308
|
+
executeSpawnBgResume(
|
|
309
|
+
plan as SpawnBgPlan,
|
|
310
|
+
lock,
|
|
311
|
+
// QB3: inject the shared lockDir-bound `release` — never the spawn factory's
|
|
312
|
+
// default releaseFn (which would release into the DEFAULT lock dir).
|
|
313
|
+
makeProductionSpawnBgResumeDeps({
|
|
314
|
+
...io.spawnOverrides,
|
|
315
|
+
killGraceMs: opts.killGraceMs ?? io.spawnOverrides.killGraceMs,
|
|
316
|
+
releaseFn: release,
|
|
317
|
+
}),
|
|
318
|
+
),
|
|
319
|
+
sendMailbox: (plan, _lock) => sendViaMailbox(plan as MetaMailboxPlan, _lock as LockClaim),
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
decide: (input: DispatchInput) => decideDispatch(input, deciderDeps),
|
|
324
|
+
executor,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entwurf-v2-release — the PURE release-policy reducer for the 5c transport hand
|
|
3
|
+
* (0.11 Stage 0 step 5c-1). It answers ONE question with zero IO: given the plan
|
|
4
|
+
* the 5b decider chose and the lock it handed over, on WHICH execution event may
|
|
5
|
+
* the transport hand release that lock — and it guarantees the release fires
|
|
6
|
+
* AT MOST ONCE.
|
|
7
|
+
*
|
|
8
|
+
* Why a pure reducer before any transport IO (5b discipline = gate-first →
|
|
9
|
+
* pure-before-IO → wire): the load-bearing risk of 5c is NOT the spawn/send IO, it
|
|
10
|
+
* is the RELEASE TIMING (Fable 3, "release-after-observation"). A spawn-bg dispatch
|
|
11
|
+
* that releases its lock too early — on spawn-started/ack instead of on an observed
|
|
12
|
+
* liveness transition — reopens the exact double-spawn window 5a's per-gid lock
|
|
13
|
+
* exists to close. So the timing logic is isolated here as a pure state machine the
|
|
14
|
+
* gate drives over every event order, and 5c-2/5c-3/5c-4 only WIRE the real
|
|
15
|
+
* send/spawn/mailbox IO onto it. The hand feeds events; this module decides release.
|
|
16
|
+
*
|
|
17
|
+
* The three policies (one per ExecutionPlan transport):
|
|
18
|
+
* - no-lock — meta-mailbox (?7: no lock was ever held).
|
|
19
|
+
* Nothing to release on any event.
|
|
20
|
+
* - release-after-send-final — control-socket send. The lock is held only for
|
|
21
|
+
* the at-most-once re-resolve; release once the send reaches a FINAL outcome.
|
|
22
|
+
* - release-after-spawn-observation — spawn-bg resume. The lock is held until the
|
|
23
|
+
* FIRST observable transition: socket-alive OR child-exited (any code). A
|
|
24
|
+
* spawn-started event is explicitly NOT a release trigger (that is the whole
|
|
25
|
+
* point). A spawn that never started (spawn-start-failed) has no child to watch,
|
|
26
|
+
* so it releases to free the gid — keeping the lock would pin it forever.
|
|
27
|
+
*
|
|
28
|
+
* The release authority is ALWAYS the LockClaim the decider handed over (5c wires it
|
|
29
|
+
* through). This module never re-derives a lock by gid, never re-resolves a path —
|
|
30
|
+
* it is pure routing logic over opaque events.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { ExecutionPlan } from "./entwurf-v2-decider.ts";
|
|
34
|
+
import type { LockClaim } from "./entwurf-v2-lock.ts";
|
|
35
|
+
|
|
36
|
+
// ── ReleasePolicy (derived once from the plan + held lock) ───────────────────
|
|
37
|
+
export type ReleasePolicy =
|
|
38
|
+
| { kind: "no-lock" }
|
|
39
|
+
| { kind: "release-after-send-final" }
|
|
40
|
+
| { kind: "release-after-spawn-observation" };
|
|
41
|
+
|
|
42
|
+
// ── ReleaseEvent (the transport hand feeds these as execution proceeds) ──────
|
|
43
|
+
// `send-final` carries the terminal send outcome (legacy parity: a fallback-sent is
|
|
44
|
+
// a real final outcome, not a hidden retry). `child-exited` carries the code (null =
|
|
45
|
+
// killed by signal) but the policy releases on ANY code. `mailbox-enqueued` is the
|
|
46
|
+
// terminal ack on the lock-free path. `spawn-started`/`spawn-start-failed` bracket
|
|
47
|
+
// the spawn attempt — only the FAILURE finalizes; a successful start must wait for an
|
|
48
|
+
// observed transition.
|
|
49
|
+
export type ReleaseEvent =
|
|
50
|
+
| { kind: "send-final"; outcome: "sent" | "fallback-sent" | "rejected" | "failed" }
|
|
51
|
+
| { kind: "spawn-started"; pid: number }
|
|
52
|
+
| { kind: "spawn-start-failed"; error: string }
|
|
53
|
+
| { kind: "socket-alive" }
|
|
54
|
+
| { kind: "child-exited"; code: number | null }
|
|
55
|
+
| { kind: "mailbox-enqueued" };
|
|
56
|
+
|
|
57
|
+
// ── ReleaseState (single-release accumulator) ───────────────────────────────
|
|
58
|
+
export interface ReleaseState {
|
|
59
|
+
released: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function initialReleaseState(): ReleaseState {
|
|
63
|
+
return { released: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* An in-domain execute (control-socket / spawn-bg) must hold a lock whose gardenId
|
|
68
|
+
* IS the plan's target. Both halves are 5b decider-contract invariants, not runtime
|
|
69
|
+
* conditions: a null lock or a lock paired with the WRONG plan are the same grade of
|
|
70
|
+
* mis-wiring (a later release would free nothing, or free a DIFFERENT gid's lock), so
|
|
71
|
+
* both fail loud here rather than silently mis-releasing. Asserts the lock non-null
|
|
72
|
+
* so the caller may use it as a `LockClaim`.
|
|
73
|
+
*/
|
|
74
|
+
function assertInDomainLock(plan: ExecutionPlan, lock: LockClaim | null): asserts lock is LockClaim {
|
|
75
|
+
if (lock === null) {
|
|
76
|
+
throw new Error(`entwurf-v2-release: an in-domain (${plan.transport}) execute must hold a lock.`);
|
|
77
|
+
}
|
|
78
|
+
if (lock.gardenId !== plan.targetGardenId) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`entwurf-v2-release: lock gardenId (${lock.gardenId}) does not match plan target (${plan.targetGardenId}) — mis-paired plan/lock.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Derive the release policy from the plan, cross-checking the lock invariants the 5b
|
|
87
|
+
* decider guarantees: meta-mailbox ⇒ lock null (?7); in-domain (control-socket /
|
|
88
|
+
* spawn-bg) ⇒ lock non-null AND lock.gardenId === plan.targetGardenId. A mismatch is
|
|
89
|
+
* a decider contract violation, not a runtime condition — it throws so a mis-wired
|
|
90
|
+
* hand fails loud instead of silently dropping or mis-releasing a lock.
|
|
91
|
+
*/
|
|
92
|
+
export function decideReleasePolicy(plan: ExecutionPlan, lock: LockClaim | null): ReleasePolicy {
|
|
93
|
+
switch (plan.transport) {
|
|
94
|
+
case "meta-mailbox":
|
|
95
|
+
if (lock !== null) {
|
|
96
|
+
throw new Error("entwurf-v2-release: a meta-mailbox plan must carry no lock (?7 invariant violated).");
|
|
97
|
+
}
|
|
98
|
+
return { kind: "no-lock" };
|
|
99
|
+
case "control-socket":
|
|
100
|
+
assertInDomainLock(plan, lock);
|
|
101
|
+
return { kind: "release-after-send-final" };
|
|
102
|
+
case "spawn-bg":
|
|
103
|
+
assertInDomainLock(plan, lock);
|
|
104
|
+
return { kind: "release-after-spawn-observation" };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ReduceReleaseResult {
|
|
109
|
+
state: ReleaseState;
|
|
110
|
+
shouldRelease: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Fold one execution event into the release decision. `shouldRelease` is true
|
|
115
|
+
* EXACTLY ONCE — on the first event that satisfies the policy's release condition;
|
|
116
|
+
* every later event (after `state.released`) returns false. The single-release
|
|
117
|
+
* guarantee is what lets the spawn watcher race socket-alive against child-exit
|
|
118
|
+
* without a double release: whichever fires first releases, the other is a no-op.
|
|
119
|
+
*/
|
|
120
|
+
export function reduceRelease(policy: ReleasePolicy, state: ReleaseState, event: ReleaseEvent): ReduceReleaseResult {
|
|
121
|
+
if (state.released) {
|
|
122
|
+
return { state, shouldRelease: false };
|
|
123
|
+
}
|
|
124
|
+
const release = (): ReduceReleaseResult => ({ state: { released: true }, shouldRelease: true });
|
|
125
|
+
const hold = (): ReduceReleaseResult => ({ state, shouldRelease: false });
|
|
126
|
+
|
|
127
|
+
switch (policy.kind) {
|
|
128
|
+
case "no-lock":
|
|
129
|
+
// No lock was ever held → nothing to release on any event.
|
|
130
|
+
return hold();
|
|
131
|
+
case "release-after-send-final":
|
|
132
|
+
// Release once the send reaches a final outcome; hold before that.
|
|
133
|
+
return event.kind === "send-final" ? release() : hold();
|
|
134
|
+
case "release-after-spawn-observation":
|
|
135
|
+
// Fable 3: spawn-started is NOT a release event. Release on the first
|
|
136
|
+
// observed transition (socket-alive / child-exited, any code), or on a
|
|
137
|
+
// failed start (no child to watch).
|
|
138
|
+
switch (event.kind) {
|
|
139
|
+
case "socket-alive":
|
|
140
|
+
case "child-exited":
|
|
141
|
+
case "spawn-start-failed":
|
|
142
|
+
return release();
|
|
143
|
+
case "spawn-started":
|
|
144
|
+
case "send-final":
|
|
145
|
+
case "mailbox-enqueued":
|
|
146
|
+
return hold();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* entwurf-v2-resume-marker — the single source of truth for the env var name that a v2
|
|
3
|
+
* spawn-bg resume plants on its resume child to authorize it as an Entwurf-child resident.
|
|
4
|
+
*
|
|
5
|
+
* Why its own leaf module (no imports): the PRODUCER is `entwurf-v2-spawn-production.ts` (a
|
|
6
|
+
* lib module that imports siblings with `.ts` extensions, compiled by `scripts/tsconfig.json`
|
|
7
|
+
* with `allowImportingTsExtensions`), but the CONSUMER is `entwurf-control.ts` (the extension
|
|
8
|
+
* entry, compiled by the ROOT tsconfig WITHOUT that flag, importing siblings as `.js`).
|
|
9
|
+
* Importing the const straight from `entwurf-v2-spawn-production.ts` into `entwurf-control.ts`
|
|
10
|
+
* would drag the whole `.ts`-importing v2 decider/spawn subtree into the root program and
|
|
11
|
+
* trip TS5097 on every `.ts` extension. A zero-import leaf const is root-safe from either side.
|
|
12
|
+
*
|
|
13
|
+
* Meaning: when this env var equals the child's own sessionId, the child is the resume citizen
|
|
14
|
+
* a v2 spawn-bg resume promoted from a dormant `entwurf`-tagged session to a live
|
|
15
|
+
* `--entwurf-control` resident (an AUTHORIZED Entwurf child resident — keeps its `entwurf` tag,
|
|
16
|
+
* stays re-resumable once it dies). A human hand-opening the same session with
|
|
17
|
+
* `--entwurf-control` carries no marker → still a "corrupt resident session name" crash. The
|
|
18
|
+
* marker is sessionId-bound (not a generic boolean) so it authorizes only the exact session it
|
|
19
|
+
* was minted for; it is a wrong-surface crash guard, not a security boundary. See
|
|
20
|
+
* `entwurf-control.ts` `maybeSetResidentName` (consumer) and `entwurf-v2-spawn-production.ts`
|
|
21
|
+
* `makeProductionSpawnBgResumeDeps` (producer).
|
|
22
|
+
*/
|
|
23
|
+
export const V2_RESUME_RESIDENT_SESSION_ENV = "ENTWURF_V2_RESUME_RESIDENT_SESSION_ID";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* True when this process is the resume child a v2 spawn-bg resume launched for EXACTLY
|
|
27
|
+
* `sessionId` — the marker equals the child's own id. The pure SSOT of the authorization
|
|
28
|
+
* check (`env` is injectable so the guard is gate-provable without touching `process.env`).
|
|
29
|
+
* The binding is to the exact id, not a generic boolean: a present-but-different marker is
|
|
30
|
+
* NOT authorized, so the env var cannot be reused to wave a different session past the guard.
|
|
31
|
+
*/
|
|
32
|
+
export function isV2ResumeResidentAuthorized(sessionId: string, env: NodeJS.ProcessEnv = process.env): boolean {
|
|
33
|
+
return env[V2_RESUME_RESIDENT_SESSION_ENV] === sessionId;
|
|
34
|
+
}
|