@loucompanion/forge-bridge 0.1.3-dev.b3bce6ba65a6 → 0.1.3-dev.ba7d36726eda
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/README.md +78 -0
- package/dist/bridge/bridge.base.d.ts +5 -1
- package/dist/bridge/bridge.service.d.ts +52 -3
- package/dist/bridge/bridge.service.js +776 -19
- package/dist/bridge/bridge.types.d.ts +131 -1
- package/dist/cleanup/cleanup.service.d.ts +13 -2
- package/dist/cleanup/cleanup.service.js +435 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.js +46 -0
- package/dist/cli/cli.service.d.ts +42 -2
- package/dist/cli/cli.service.js +350 -23
- package/dist/cli/local-config.d.ts +4 -0
- package/dist/cli/local-config.js +39 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +15 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +50 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +74 -0
- package/dist/runtime/attempt-process-supervisor.js +290 -0
- package/dist/runtime/host-listener-inspector.d.ts +28 -0
- package/dist/runtime/host-listener-inspector.js +185 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +39 -0
- package/dist/runtime/host-runtime-contract.js +295 -0
- package/dist/runtime/host-runtime-lease.d.ts +122 -0
- package/dist/runtime/host-runtime-lease.js +1241 -0
- package/dist/runtime/host-runtime-promotion.d.ts +21 -0
- package/dist/runtime/host-runtime-promotion.js +128 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +44 -0
- package/dist/runtime/node-host-runtime-adapter.js +205 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +136 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +45 -0
- package/dist/sandbox/preflight.js +300 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +119 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +86 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +67 -0
- package/dist/session/phase-completion.artifacts.js +23 -2
- package/dist/session/phase-completion.helpers.js +6 -1
- package/dist/session/phase-completion.runtime.js +3 -1
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.artifacts.js +28 -4
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +2 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +20 -9
- package/dist/session/provider-cli/provider-cli.process.d.ts +3 -0
- package/dist/session/provider-cli/provider-cli.process.js +65 -14
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +14 -2
- package/dist/session/provider-cli/provider-cli.prompts.js +99 -15
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +1 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +3 -2
- package/dist/session/provider-cli/provider-cli.service.d.ts +4 -1
- package/dist/session/provider-cli/provider-cli.service.js +167 -28
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +23 -0
- package/dist/session/session.base.d.ts +4 -0
- package/dist/session/session.service.d.ts +6 -1
- package/dist/session/session.service.js +184 -38
- package/dist/session/session.types.d.ts +58 -1
- package/dist/session/session.types.js +172 -2
- package/dist/shared/path/path.js +3 -1
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +40 -0
- package/dist/shared/process/host-process-supervisor.js +195 -0
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/index.d.ts +1 -0
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +6 -2
- package/dist/worktree/worktree.service.js +60 -43
- package/dist/worktree/worktree.types.d.ts +3 -1
- package/package.json +6 -3
- package/runtime/windows-job-supervisor.ps1 +287 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { redactRunnerDispatchEvent, redactRunOnceResultPacket } from "./session-redaction.js";
|
|
2
2
|
import { RoutingProviderCliService } from "./provider-cli/provider-cli.service.js";
|
|
3
3
|
import { GitWorktreeService } from "../worktree/worktree.service.js";
|
|
4
|
+
import { SandboxWorkspaceService } from "../sandbox/workspace.service.js";
|
|
5
|
+
import { HostRuntimeAdmissionExpiredError, hostRuntimeEventResources, } from "../runtime/host-runtime-lease.js";
|
|
4
6
|
export class BridgeSessionService {
|
|
5
7
|
worktrees;
|
|
6
8
|
providerCli;
|
|
7
|
-
|
|
9
|
+
hostRuntime;
|
|
10
|
+
constructor(worktrees = process.env.FORGE_SANDBOX_ID
|
|
11
|
+
? new SandboxWorkspaceService()
|
|
12
|
+
: new GitWorktreeService(), providerCli = new RoutingProviderCliService(), hostRuntime) {
|
|
8
13
|
this.worktrees = worktrees;
|
|
9
14
|
this.providerCli = providerCli;
|
|
15
|
+
this.hostRuntime = hostRuntime;
|
|
16
|
+
}
|
|
17
|
+
async updateLease(dispatchAttemptId, fencingToken, expiresAt) {
|
|
18
|
+
await this.hostRuntime?.updateLease(dispatchAttemptId, fencingToken, expiresAt);
|
|
19
|
+
}
|
|
20
|
+
async reapHostRuntime(options) {
|
|
21
|
+
return await this.hostRuntime?.reap?.(options) ?? null;
|
|
22
|
+
}
|
|
23
|
+
runtimeCapacity() {
|
|
24
|
+
return this.hostRuntime?.snapshot?.() ?? null;
|
|
10
25
|
}
|
|
11
26
|
async runOnce(dispatch, sink, signal) {
|
|
12
27
|
validateApiKeySecretGrant(dispatch);
|
|
@@ -16,7 +31,12 @@ export class BridgeSessionService {
|
|
|
16
31
|
if (signal?.aborted) {
|
|
17
32
|
return;
|
|
18
33
|
}
|
|
19
|
-
const redacted =
|
|
34
|
+
const redacted = redactRunnerDispatchEvent(event, redactionSecrets);
|
|
35
|
+
events.push(redacted);
|
|
36
|
+
await sink.emit(redacted);
|
|
37
|
+
};
|
|
38
|
+
const emitLifecycle = async (event) => {
|
|
39
|
+
const redacted = redactRunnerDispatchEvent(event, redactionSecrets);
|
|
20
40
|
events.push(redacted);
|
|
21
41
|
await sink.emit(redacted);
|
|
22
42
|
};
|
|
@@ -27,44 +47,121 @@ export class BridgeSessionService {
|
|
|
27
47
|
content: "forge-bridge accepted run-once dispatch.",
|
|
28
48
|
eventType: "dispatch_accepted"
|
|
29
49
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
|
|
34
|
-
worktreeKey: dispatch.lease.worktreeKey,
|
|
35
|
-
ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
|
|
36
|
-
ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
|
|
37
|
-
lane: dispatch.execution.lane,
|
|
38
|
-
phase: dispatch.cycleRun.phase,
|
|
39
|
-
allowedRepoRoot: dispatch.serverLocal.repoRoot,
|
|
40
|
-
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
|
|
41
|
-
artifacts: dispatch.artifacts,
|
|
42
|
-
qaRework: dispatch.qaRework,
|
|
43
|
-
warmPathOverrides: dispatch.warmPathOverrides
|
|
44
|
-
});
|
|
45
|
-
const providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
|
|
46
|
-
throwIfAborted(signal);
|
|
47
|
-
await emit({
|
|
48
|
-
role: "runner",
|
|
49
|
-
kind: "checkpoint",
|
|
50
|
-
content: `Prepared worktree ${worktree.branchName}.`,
|
|
51
|
-
eventType: "worktree_prepared"
|
|
52
|
-
});
|
|
53
|
-
if (worktree.dependencyWarm) {
|
|
54
|
-
await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
|
|
55
|
-
}
|
|
50
|
+
let worktree = null;
|
|
51
|
+
let hostSingletonKeys = null;
|
|
52
|
+
let hostLease = null;
|
|
56
53
|
try {
|
|
54
|
+
if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
|
|
55
|
+
await emitLifecycle({
|
|
56
|
+
role: "runner",
|
|
57
|
+
kind: "checkpoint",
|
|
58
|
+
content: "Evaluating runner-local managed host admission before preparing the worktree.",
|
|
59
|
+
eventType: "host_runtime_admission_waiting",
|
|
60
|
+
});
|
|
61
|
+
try {
|
|
62
|
+
hostSingletonKeys = await this.hostRuntime.acquire(dispatch, signal);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof HostRuntimeAdmissionExpiredError) {
|
|
66
|
+
await emitLifecycle({
|
|
67
|
+
role: "runner",
|
|
68
|
+
kind: "error",
|
|
69
|
+
content: "Managed host admission expired while the project singleton remained busy; no worktree or runtime resources were created.",
|
|
70
|
+
eventType: "host_runtime_admission_expired",
|
|
71
|
+
}).catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
throwIfAborted(signal);
|
|
76
|
+
await emitLifecycle({
|
|
77
|
+
role: "runner",
|
|
78
|
+
kind: "checkpoint",
|
|
79
|
+
content: hostSingletonKeys.length > 0
|
|
80
|
+
? `Acquired managed host singleton ${hostSingletonKeys.join(", ")}.`
|
|
81
|
+
: "Observed assigned-port evidence permits provisional parallel host admission; the prepared runtime contract must still match before launch.",
|
|
82
|
+
eventType: "host_runtime_admission_acquired",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
worktree = await this.worktrees.prepare({
|
|
86
|
+
repo: dispatch.repo,
|
|
87
|
+
workItem: dispatch.workItem,
|
|
88
|
+
baseBranch: dispatch.cycleRun.baseBranch || dispatch.cycleRun.requestedBase,
|
|
89
|
+
worktreeKey: dispatch.lease.worktreeKey,
|
|
90
|
+
ownerType: dispatch.cycleRun.ownerType ?? dispatch.lease.ownerType ?? null,
|
|
91
|
+
ownerId: dispatch.cycleRun.ownerId ?? dispatch.lease.ownerId ?? null,
|
|
92
|
+
lane: dispatch.execution.lane,
|
|
93
|
+
phase: dispatch.cycleRun.phase,
|
|
94
|
+
allowedRepoRoot: dispatch.serverLocal.repoRoot,
|
|
95
|
+
cleanupWorktrees: dispatch.serverLocal.cleanupWorktrees,
|
|
96
|
+
artifacts: dispatch.artifacts,
|
|
97
|
+
priorPhaseArtifacts: dispatch.priorPhaseArtifacts,
|
|
98
|
+
qaRework: dispatch.qaRework,
|
|
99
|
+
warmPathOverrides: dispatch.warmPathOverrides
|
|
100
|
+
});
|
|
101
|
+
const providerDispatch = withPreparedQaReworkPacket(dispatch, worktree.qaReworkPacketPath);
|
|
102
|
+
throwIfAborted(signal);
|
|
103
|
+
await emit({
|
|
104
|
+
role: "runner",
|
|
105
|
+
kind: "checkpoint",
|
|
106
|
+
content: `Prepared worktree ${worktree.branchName}.`,
|
|
107
|
+
eventType: "worktree_prepared"
|
|
108
|
+
});
|
|
109
|
+
if ((worktree.hydratedPriorPhaseArtifactCount ?? 0) > 0) {
|
|
110
|
+
await emit({
|
|
111
|
+
role: "runner",
|
|
112
|
+
kind: "checkpoint",
|
|
113
|
+
content: `Hydrated ${worktree.hydratedPriorPhaseArtifactCount} persisted prior-phase artifact(s) into the prepared worktree.`,
|
|
114
|
+
eventType: "prior_phase_artifacts_hydrated"
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (worktree.dependencyWarm) {
|
|
118
|
+
await emitDependencyWarmEvents(worktree.dependencyWarm, emit);
|
|
119
|
+
}
|
|
120
|
+
if (dispatch.sandbox?.executionMode === "host" && this.hostRuntime) {
|
|
121
|
+
hostLease = await this.hostRuntime.start(dispatch, worktree.worktreePath, signal);
|
|
122
|
+
await emitLifecycle({
|
|
123
|
+
role: "runner",
|
|
124
|
+
kind: "checkpoint",
|
|
125
|
+
content: `Host runtime lease ${hostLease.lease.leaseId} is active for this worktree.`,
|
|
126
|
+
eventType: "sandbox_started",
|
|
127
|
+
occurredAt: hostLease.lease.stateChangedAt,
|
|
128
|
+
data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract)),
|
|
129
|
+
});
|
|
130
|
+
await emitLifecycle({
|
|
131
|
+
role: "runner",
|
|
132
|
+
kind: "checkpoint",
|
|
133
|
+
content: "Managed host fallback isolates the worktree, attempt paths, assigned loopback ports, and supervised process trees, but not the host filesystem, network, credentials, or kernel. Serialized projects also hold a runner-local singleton. Enable Run in Docker in Project Settings when stronger isolation is required.",
|
|
134
|
+
eventType: "host_runtime_isolation_notice",
|
|
135
|
+
data: JSON.stringify({
|
|
136
|
+
isolationStrength: "managed_host_process",
|
|
137
|
+
remediation: "Enable Run in Docker in Project Settings.",
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
const unconfiguredPortCount = hostLease.lease.resources.ports.length;
|
|
141
|
+
hostLease = await this.hostRuntime.startConfiguredProject(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken, signal);
|
|
142
|
+
if (hostLease.lease.resources.ports.length > unconfiguredPortCount) {
|
|
143
|
+
const assigned = hostLease.lease.resources.ports.at(-1);
|
|
144
|
+
await emitLifecycle({
|
|
145
|
+
role: "runner",
|
|
146
|
+
kind: "checkpoint",
|
|
147
|
+
content: `Verified supervised project runtime ${assigned.name} on ${assigned.hostIp}:${assigned.hostPort}.`,
|
|
148
|
+
eventType: "host_runtime_project_ready",
|
|
149
|
+
data: JSON.stringify(hostRuntimeEventResources(hostLease.lease, hostLease.runtimeContract)),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
57
153
|
if (dispatch.draft) {
|
|
58
154
|
throwIfAborted(signal);
|
|
59
155
|
const run = await this.providerCli.runDraft({
|
|
60
156
|
dispatch: providerDispatch,
|
|
61
157
|
worktree,
|
|
158
|
+
runtimeEnvironment: hostLease?.environment,
|
|
62
159
|
emit,
|
|
63
160
|
signal
|
|
64
161
|
});
|
|
65
162
|
throwIfAborted(signal);
|
|
66
163
|
const draftArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
|
|
67
|
-
return
|
|
164
|
+
return redactRunOnceResultPacket({
|
|
68
165
|
schemaVersion: 1,
|
|
69
166
|
exitCode: 0,
|
|
70
167
|
summary: run.summary,
|
|
@@ -75,14 +172,17 @@ export class BridgeSessionService {
|
|
|
75
172
|
}, redactionSecrets);
|
|
76
173
|
}
|
|
77
174
|
throwIfAborted(signal);
|
|
78
|
-
const lane = dispatch.execution.lane.toLowerCase();
|
|
79
|
-
const role = dispatch.execution.role.toLowerCase();
|
|
175
|
+
const lane = dispatch.execution.lane.trim().toLowerCase();
|
|
176
|
+
const role = dispatch.execution.role.trim().toLowerCase();
|
|
177
|
+
const cycleKind = dispatch.cycleRun.kind.trim().toLowerCase();
|
|
80
178
|
const isQa = role === "qa" || lane === "qa";
|
|
81
|
-
const isArchitect = role === "architect" || lane === "architect" ||
|
|
179
|
+
const isArchitect = role === "architect" || lane === "architect" || cycleKind === "architect";
|
|
180
|
+
const isCiRepair = role === "ci_repair" || lane === "ci_repair" || cycleKind === "pr_checks";
|
|
82
181
|
const isConflictResolve = role === "conflict_resolve" || lane === "conflict_resolve";
|
|
83
182
|
const context = {
|
|
84
183
|
dispatch: providerDispatch,
|
|
85
184
|
worktree,
|
|
185
|
+
runtimeEnvironment: hostLease?.environment,
|
|
86
186
|
emit,
|
|
87
187
|
signal
|
|
88
188
|
};
|
|
@@ -90,12 +190,14 @@ export class BridgeSessionService {
|
|
|
90
190
|
? await this.providerCli.runQa(context)
|
|
91
191
|
: isArchitect
|
|
92
192
|
? await this.providerCli.runArchitect(context)
|
|
93
|
-
:
|
|
94
|
-
? await this.providerCli.
|
|
95
|
-
:
|
|
193
|
+
: isCiRepair
|
|
194
|
+
? await this.providerCli.runCiRepair(context)
|
|
195
|
+
: isConflictResolve
|
|
196
|
+
? await this.providerCli.runConflictResolve(context)
|
|
197
|
+
: await this.providerCli.runImplementation(context);
|
|
96
198
|
throwIfAborted(signal);
|
|
97
199
|
const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
|
|
98
|
-
return
|
|
200
|
+
return redactRunOnceResultPacket({
|
|
99
201
|
schemaVersion: 1,
|
|
100
202
|
exitCode: 0,
|
|
101
203
|
summary: run.summary,
|
|
@@ -106,7 +208,51 @@ export class BridgeSessionService {
|
|
|
106
208
|
}, redactionSecrets);
|
|
107
209
|
}
|
|
108
210
|
finally {
|
|
109
|
-
|
|
211
|
+
if (hostLease && dispatch.sandbox) {
|
|
212
|
+
const teardownAt = new Date().toISOString();
|
|
213
|
+
await emitLifecycle({
|
|
214
|
+
role: "runner",
|
|
215
|
+
kind: "checkpoint",
|
|
216
|
+
content: `Releasing host runtime lease ${hostLease.lease.leaseId}.`,
|
|
217
|
+
eventType: "sandbox_teardown_started",
|
|
218
|
+
occurredAt: teardownAt,
|
|
219
|
+
}).catch(() => undefined);
|
|
220
|
+
try {
|
|
221
|
+
const released = await this.hostRuntime?.release(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken);
|
|
222
|
+
if (!released) {
|
|
223
|
+
await emitLifecycle({
|
|
224
|
+
role: "runner",
|
|
225
|
+
kind: "error",
|
|
226
|
+
content: "Host runtime cleanup requires reconciliation: the runtime lease was not current during release.",
|
|
227
|
+
eventType: "teardown_failed",
|
|
228
|
+
occurredAt: new Date().toISOString(),
|
|
229
|
+
}).catch(() => undefined);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
await emitLifecycle({
|
|
233
|
+
role: "runner",
|
|
234
|
+
kind: "checkpoint",
|
|
235
|
+
content: `Released host runtime lease ${released.leaseId}.`,
|
|
236
|
+
eventType: "sandbox_teardown_completed",
|
|
237
|
+
occurredAt: released.releasedAt,
|
|
238
|
+
}).catch(() => undefined);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
await emitLifecycle({
|
|
243
|
+
role: "runner",
|
|
244
|
+
kind: "error",
|
|
245
|
+
content: `Host runtime cleanup requires reconciliation: ${error instanceof Error ? error.message : String(error)}`,
|
|
246
|
+
eventType: "teardown_failed",
|
|
247
|
+
occurredAt: new Date().toISOString(),
|
|
248
|
+
}).catch(() => undefined);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (!hostLease && hostSingletonKeys && dispatch.sandbox) {
|
|
252
|
+
await this.hostRuntime?.release(dispatch.sandbox.dispatchAttemptId, dispatch.lease.fencingToken).catch(() => undefined);
|
|
253
|
+
}
|
|
254
|
+
if (worktree)
|
|
255
|
+
await this.worktrees.release(worktree);
|
|
110
256
|
}
|
|
111
257
|
}
|
|
112
258
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface RunOnceDispatchPacket {
|
|
2
|
-
readonly schemaVersion: 1;
|
|
2
|
+
readonly schemaVersion: 1 | 2;
|
|
3
3
|
readonly userId: string;
|
|
4
4
|
readonly workItem: DispatchWorkItem;
|
|
5
5
|
readonly repo: DispatchRepo;
|
|
@@ -8,9 +8,12 @@ export interface RunOnceDispatchPacket {
|
|
|
8
8
|
readonly lease: DispatchLease;
|
|
9
9
|
readonly execution: DispatchExecution;
|
|
10
10
|
readonly serverLocal: DispatchServerLocalConfig;
|
|
11
|
+
readonly sandbox?: DispatchSandboxContext | null;
|
|
11
12
|
readonly pullRequest?: DispatchPullRequestContext | null;
|
|
12
13
|
readonly apiKeySecretGrant?: ApiKeySecretGrant | null;
|
|
14
|
+
readonly apiKeySecretGrantHandle?: ApiKeySecretGrantHandle | null;
|
|
13
15
|
readonly artifacts: readonly DispatchResolvedArtifact[];
|
|
16
|
+
readonly priorPhaseArtifacts?: readonly DispatchPriorPhaseArtifact[];
|
|
14
17
|
readonly warmPathOverrides?: readonly string[];
|
|
15
18
|
readonly qaBrowser?: DispatchQaBrowserContext | null;
|
|
16
19
|
readonly qaScope?: DispatchQaScope | null;
|
|
@@ -22,6 +25,42 @@ export interface RunOnceDispatchPacket {
|
|
|
22
25
|
readonly designerRefs?: DispatchDesignerRefHandoffContext | null;
|
|
23
26
|
readonly draft?: DispatchDraftPrompt | null;
|
|
24
27
|
}
|
|
28
|
+
export interface DispatchSandboxContext {
|
|
29
|
+
readonly dispatchAttemptId: string;
|
|
30
|
+
readonly executionLeaseId: string;
|
|
31
|
+
readonly runnerInstanceId: string;
|
|
32
|
+
readonly executionMode: "host" | "docker";
|
|
33
|
+
readonly executionPolicy: "host_allowed" | "sandbox_preferred" | "sandbox_required";
|
|
34
|
+
readonly networkPolicy: DispatchSandboxNetworkPolicy;
|
|
35
|
+
readonly requestedResources: DispatchSandboxResourceRequest;
|
|
36
|
+
readonly runtime?: string | null;
|
|
37
|
+
readonly sandboxConfigCommitSha?: string | null;
|
|
38
|
+
readonly inputCommitSha?: string | null;
|
|
39
|
+
readonly contractDigest?: string | null;
|
|
40
|
+
readonly imageDigest?: string | null;
|
|
41
|
+
readonly publicationMode: "bridge_push" | "shared_filesystem";
|
|
42
|
+
readonly publishGrantHandle?: DispatchPublishGrantHandle | null;
|
|
43
|
+
}
|
|
44
|
+
export interface DispatchPublishGrantHandle {
|
|
45
|
+
readonly grantId: string;
|
|
46
|
+
readonly branch: string;
|
|
47
|
+
readonly expiresAt: string;
|
|
48
|
+
}
|
|
49
|
+
export interface DispatchSandboxResourceRequest {
|
|
50
|
+
readonly sandboxSlots: number;
|
|
51
|
+
readonly cpuMillis: number;
|
|
52
|
+
readonly workerMemoryLimitBytes: number;
|
|
53
|
+
readonly serviceMemoryLimitBytes: number;
|
|
54
|
+
readonly runtimeOverheadBytes: number;
|
|
55
|
+
readonly memoryBytes: number;
|
|
56
|
+
readonly workspaceBytes: number;
|
|
57
|
+
readonly pids: number;
|
|
58
|
+
readonly buildSlots: number;
|
|
59
|
+
}
|
|
60
|
+
export interface DispatchSandboxNetworkPolicy {
|
|
61
|
+
readonly mode: "offline" | "public_allowlist" | "open_public";
|
|
62
|
+
readonly allowedHosts: readonly string[];
|
|
63
|
+
}
|
|
25
64
|
export interface DispatchDraftPrompt {
|
|
26
65
|
readonly kind: string;
|
|
27
66
|
readonly systemPrompt: string;
|
|
@@ -154,6 +193,19 @@ export interface DispatchStoryQaDependencyContext {
|
|
|
154
193
|
readonly fromWorkItemId: string;
|
|
155
194
|
readonly toWorkItemId: string;
|
|
156
195
|
}
|
|
196
|
+
export interface DispatchPriorPhaseArtifact {
|
|
197
|
+
readonly workItemId: string;
|
|
198
|
+
readonly artifactId: string;
|
|
199
|
+
readonly cycleRunId: string;
|
|
200
|
+
readonly phase: "implementation" | "self_review" | "validation";
|
|
201
|
+
readonly status: string;
|
|
202
|
+
readonly producedAt: string;
|
|
203
|
+
readonly sourcePath: string;
|
|
204
|
+
readonly verdictJson: string;
|
|
205
|
+
readonly report: string;
|
|
206
|
+
readonly verdictContentHash: string;
|
|
207
|
+
readonly reportContentHash: string;
|
|
208
|
+
}
|
|
157
209
|
export interface DispatchDesignerRefHandoffContext {
|
|
158
210
|
readonly schemaVersion: number;
|
|
159
211
|
readonly kind: string;
|
|
@@ -279,6 +331,11 @@ export interface ApiKeySecretGrant {
|
|
|
279
331
|
readonly secret: string;
|
|
280
332
|
readonly expiresAt: string;
|
|
281
333
|
}
|
|
334
|
+
export interface ApiKeySecretGrantHandle {
|
|
335
|
+
readonly grantId: string;
|
|
336
|
+
readonly provider: string;
|
|
337
|
+
readonly expiresAt: string;
|
|
338
|
+
}
|
|
282
339
|
export interface DispatchServerLocalConfig {
|
|
283
340
|
readonly repoRoot: string;
|
|
284
341
|
readonly cleanupWorktrees: boolean;
|
|
@@ -2,11 +2,11 @@ import path from "node:path";
|
|
|
2
2
|
export function parseRunOnceDispatchPacket(value) {
|
|
3
3
|
const packet = asObject(value, "dispatch packet");
|
|
4
4
|
const schemaVersion = requiredNumber(packet, "schemaVersion");
|
|
5
|
-
if (schemaVersion !== 1) {
|
|
5
|
+
if (schemaVersion !== 1 && schemaVersion !== 2) {
|
|
6
6
|
throw new Error(`Unsupported dispatch schemaVersion: ${schemaVersion}`);
|
|
7
7
|
}
|
|
8
8
|
return {
|
|
9
|
-
schemaVersion
|
|
9
|
+
schemaVersion,
|
|
10
10
|
userId: requiredString(packet, "userId"),
|
|
11
11
|
workItem: parseWorkItem(packet.workItem),
|
|
12
12
|
repo: parseRepo(packet.repo),
|
|
@@ -15,9 +15,12 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
15
15
|
lease: parseLease(packet.lease),
|
|
16
16
|
execution: parseExecution(packet.execution),
|
|
17
17
|
serverLocal: parseServerLocal(packet.serverLocal),
|
|
18
|
+
sandbox: parseSandbox(packet.sandbox, schemaVersion),
|
|
18
19
|
pullRequest: parsePullRequest(packet.pullRequest),
|
|
19
20
|
apiKeySecretGrant: parseApiKeySecretGrant(packet.apiKeySecretGrant),
|
|
21
|
+
apiKeySecretGrantHandle: parseApiKeySecretGrantHandle(packet.apiKeySecretGrantHandle),
|
|
20
22
|
artifacts: parseArtifacts(packet.artifacts),
|
|
23
|
+
priorPhaseArtifacts: parsePriorPhaseArtifacts(packet.priorPhaseArtifacts),
|
|
21
24
|
warmPathOverrides: parseWarmPathOverrides(packet.warmPathOverrides),
|
|
22
25
|
qaBrowser: parseQaBrowser(packet.qaBrowser),
|
|
23
26
|
qaScope: parseQaScope(packet.qaScope ?? packet.qa_scope),
|
|
@@ -30,6 +33,128 @@ export function parseRunOnceDispatchPacket(value) {
|
|
|
30
33
|
draft: parseDraftPrompt(packet.draft)
|
|
31
34
|
};
|
|
32
35
|
}
|
|
36
|
+
function parseSandbox(value, schemaVersion) {
|
|
37
|
+
if (value === undefined || value === null) {
|
|
38
|
+
if (schemaVersion === 2) {
|
|
39
|
+
throw new Error("sandbox is required for dispatch schemaVersion 2.");
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const sandbox = asObject(value, "sandbox");
|
|
44
|
+
const executionMode = requiredString(sandbox, "executionMode");
|
|
45
|
+
if (executionMode !== "host" && executionMode !== "docker") {
|
|
46
|
+
throw new Error("sandbox.executionMode must be host or docker.");
|
|
47
|
+
}
|
|
48
|
+
const executionPolicy = requiredString(sandbox, "executionPolicy");
|
|
49
|
+
if (executionPolicy !== "host_allowed" && executionPolicy !== "sandbox_preferred" && executionPolicy !== "sandbox_required") {
|
|
50
|
+
throw new Error("sandbox.executionPolicy is invalid.");
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
dispatchAttemptId: requiredString(sandbox, "dispatchAttemptId"),
|
|
54
|
+
executionLeaseId: requiredString(sandbox, "executionLeaseId"),
|
|
55
|
+
runnerInstanceId: requiredString(sandbox, "runnerInstanceId"),
|
|
56
|
+
executionMode,
|
|
57
|
+
executionPolicy,
|
|
58
|
+
networkPolicy: parseSandboxNetworkPolicy(sandbox.networkPolicy),
|
|
59
|
+
requestedResources: parseSandboxResourceRequest(sandbox.requestedResources),
|
|
60
|
+
runtime: optionalString(sandbox, "runtime"),
|
|
61
|
+
sandboxConfigCommitSha: optionalString(sandbox, "sandboxConfigCommitSha"),
|
|
62
|
+
inputCommitSha: optionalString(sandbox, "inputCommitSha"),
|
|
63
|
+
contractDigest: optionalString(sandbox, "contractDigest"),
|
|
64
|
+
imageDigest: optionalString(sandbox, "imageDigest"),
|
|
65
|
+
publicationMode: parsePublicationMode(sandbox.publicationMode),
|
|
66
|
+
publishGrantHandle: parsePublishGrantHandle(sandbox.publishGrantHandle)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function parsePublicationMode(value) {
|
|
70
|
+
if (value === undefined || value === null)
|
|
71
|
+
return "shared_filesystem";
|
|
72
|
+
if (value !== "bridge_push" && value !== "shared_filesystem")
|
|
73
|
+
throw new Error("sandbox.publicationMode is invalid.");
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
function parsePublishGrantHandle(value) {
|
|
77
|
+
if (value === undefined || value === null)
|
|
78
|
+
return null;
|
|
79
|
+
const handle = asObject(value, "sandbox.publishGrantHandle");
|
|
80
|
+
const grantId = requiredString(handle, "grantId");
|
|
81
|
+
const branch = requiredString(handle, "branch");
|
|
82
|
+
const expiresAt = requiredString(handle, "expiresAt");
|
|
83
|
+
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu.test(grantId)) {
|
|
84
|
+
throw new Error("sandbox.publishGrantHandle.grantId is invalid.");
|
|
85
|
+
}
|
|
86
|
+
if (branch.length > 240)
|
|
87
|
+
throw new Error("sandbox.publishGrantHandle.branch is too long.");
|
|
88
|
+
if (!Number.isFinite(Date.parse(expiresAt)))
|
|
89
|
+
throw new Error("sandbox.publishGrantHandle.expiresAt is invalid.");
|
|
90
|
+
return { grantId, branch, expiresAt };
|
|
91
|
+
}
|
|
92
|
+
function parseSandboxResourceRequest(value) {
|
|
93
|
+
if (value === undefined || value === null) {
|
|
94
|
+
return {
|
|
95
|
+
sandboxSlots: 1,
|
|
96
|
+
cpuMillis: 2000,
|
|
97
|
+
workerMemoryLimitBytes: 2 * 1024 ** 3,
|
|
98
|
+
serviceMemoryLimitBytes: 0,
|
|
99
|
+
runtimeOverheadBytes: 128 * 1024 ** 2,
|
|
100
|
+
memoryBytes: 2 * 1024 ** 3 + 128 * 1024 ** 2,
|
|
101
|
+
workspaceBytes: 10 * 1024 ** 3,
|
|
102
|
+
pids: 512,
|
|
103
|
+
buildSlots: 1
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const resources = asObject(value, "sandbox.requestedResources");
|
|
107
|
+
const memoryBytes = boundedInteger(resources, "memoryBytes", 128 * 1024 ** 2, 512 * 1024 ** 3);
|
|
108
|
+
const hasMemoryBreakdown = ["workerMemoryLimitBytes", "serviceMemoryLimitBytes", "runtimeOverheadBytes"]
|
|
109
|
+
.some(key => resources[key] !== undefined && resources[key] !== null);
|
|
110
|
+
const workerMemoryLimitBytes = hasMemoryBreakdown
|
|
111
|
+
? boundedInteger(resources, "workerMemoryLimitBytes", 128 * 1024 ** 2, 512 * 1024 ** 3)
|
|
112
|
+
: memoryBytes;
|
|
113
|
+
const serviceMemoryLimitBytes = hasMemoryBreakdown
|
|
114
|
+
? boundedInteger(resources, "serviceMemoryLimitBytes", 0, 512 * 1024 ** 3)
|
|
115
|
+
: 0;
|
|
116
|
+
const runtimeOverheadBytes = hasMemoryBreakdown
|
|
117
|
+
? boundedInteger(resources, "runtimeOverheadBytes", 0, 64 * 1024 ** 3)
|
|
118
|
+
: 0;
|
|
119
|
+
if (workerMemoryLimitBytes + serviceMemoryLimitBytes + runtimeOverheadBytes !== memoryBytes) {
|
|
120
|
+
throw new Error("sandbox.requestedResources memory breakdown must equal memoryBytes.");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
sandboxSlots: boundedInteger(resources, "sandboxSlots", 1, 4),
|
|
124
|
+
cpuMillis: boundedInteger(resources, "cpuMillis", 100, 64_000),
|
|
125
|
+
workerMemoryLimitBytes,
|
|
126
|
+
serviceMemoryLimitBytes,
|
|
127
|
+
runtimeOverheadBytes,
|
|
128
|
+
memoryBytes,
|
|
129
|
+
workspaceBytes: boundedInteger(resources, "workspaceBytes", 1024 ** 3, 10 * 1024 ** 4),
|
|
130
|
+
pids: boundedInteger(resources, "pids", 32, 65_536),
|
|
131
|
+
buildSlots: boundedInteger(resources, "buildSlots", 0, 4)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function parseSandboxNetworkPolicy(value) {
|
|
135
|
+
if (value === undefined || value === null)
|
|
136
|
+
return { mode: "offline", allowedHosts: [] };
|
|
137
|
+
const policy = asObject(value, "sandbox.networkPolicy");
|
|
138
|
+
const mode = requiredString(policy, "mode");
|
|
139
|
+
if (mode !== "offline" && mode !== "public_allowlist" && mode !== "open_public") {
|
|
140
|
+
throw new Error("sandbox.networkPolicy.mode is invalid.");
|
|
141
|
+
}
|
|
142
|
+
const rawHosts = policy.allowedHosts ?? [];
|
|
143
|
+
if (!Array.isArray(rawHosts) || rawHosts.some((host) => typeof host !== "string")) {
|
|
144
|
+
throw new Error("sandbox.networkPolicy.allowedHosts must be a string array.");
|
|
145
|
+
}
|
|
146
|
+
const allowedHosts = [...new Set(rawHosts.map(host => host.trim().toLowerCase()).filter(Boolean))];
|
|
147
|
+
if (allowedHosts.length > 128 || allowedHosts.some(host => !/^(?:\*\.)?(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(host))) {
|
|
148
|
+
throw new Error("sandbox.networkPolicy.allowedHosts contains an invalid hostname.");
|
|
149
|
+
}
|
|
150
|
+
if (mode !== "public_allowlist" && allowedHosts.length > 0) {
|
|
151
|
+
throw new Error("sandbox.networkPolicy.allowedHosts requires public_allowlist mode.");
|
|
152
|
+
}
|
|
153
|
+
if (mode === "public_allowlist" && allowedHosts.length === 0) {
|
|
154
|
+
return { mode: "offline", allowedHosts: [] };
|
|
155
|
+
}
|
|
156
|
+
return { mode, allowedHosts };
|
|
157
|
+
}
|
|
33
158
|
export function failureResult(summary, events = []) {
|
|
34
159
|
return {
|
|
35
160
|
schemaVersion: 1,
|
|
@@ -172,6 +297,16 @@ function parseApiKeySecretGrant(value) {
|
|
|
172
297
|
expiresAt: requiredString(grant, "expiresAt")
|
|
173
298
|
};
|
|
174
299
|
}
|
|
300
|
+
function parseApiKeySecretGrantHandle(value) {
|
|
301
|
+
if (value === undefined || value === null)
|
|
302
|
+
return null;
|
|
303
|
+
const grant = asObject(value, "apiKeySecretGrantHandle");
|
|
304
|
+
return {
|
|
305
|
+
grantId: requiredString(grant, "grantId"),
|
|
306
|
+
provider: requiredString(grant, "provider"),
|
|
307
|
+
expiresAt: requiredString(grant, "expiresAt")
|
|
308
|
+
};
|
|
309
|
+
}
|
|
175
310
|
function parseServerLocal(value) {
|
|
176
311
|
const serverLocal = asObject(value, "serverLocal");
|
|
177
312
|
return {
|
|
@@ -549,6 +684,34 @@ function parseArtifacts(value) {
|
|
|
549
684
|
};
|
|
550
685
|
});
|
|
551
686
|
}
|
|
687
|
+
function parsePriorPhaseArtifacts(value) {
|
|
688
|
+
if (value === undefined || value === null) {
|
|
689
|
+
return [];
|
|
690
|
+
}
|
|
691
|
+
if (!Array.isArray(value)) {
|
|
692
|
+
throw new Error("priorPhaseArtifacts must be an array when provided.");
|
|
693
|
+
}
|
|
694
|
+
return value.map((entry, index) => {
|
|
695
|
+
const artifact = asObject(entry, `priorPhaseArtifacts[${index}]`);
|
|
696
|
+
const phase = requiredString(artifact, "phase").trim().toLowerCase().replace(/-/g, "_");
|
|
697
|
+
if (phase !== "implementation" && phase !== "self_review" && phase !== "validation") {
|
|
698
|
+
throw new Error(`priorPhaseArtifacts[${index}].phase must be implementation, self_review, or validation.`);
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
workItemId: requiredString(artifact, "workItemId"),
|
|
702
|
+
artifactId: requiredString(artifact, "artifactId"),
|
|
703
|
+
cycleRunId: requiredString(artifact, "cycleRunId"),
|
|
704
|
+
phase,
|
|
705
|
+
status: requiredString(artifact, "status"),
|
|
706
|
+
producedAt: requiredString(artifact, "producedAt"),
|
|
707
|
+
sourcePath: requiredString(artifact, "sourcePath"),
|
|
708
|
+
verdictJson: requiredString(artifact, "verdictJson"),
|
|
709
|
+
report: optionalString(artifact, "report") ?? "",
|
|
710
|
+
verdictContentHash: requiredString(artifact, "verdictContentHash").toLowerCase(),
|
|
711
|
+
reportContentHash: requiredString(artifact, "reportContentHash").toLowerCase()
|
|
712
|
+
};
|
|
713
|
+
});
|
|
714
|
+
}
|
|
552
715
|
function asObject(value, label) {
|
|
553
716
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
554
717
|
throw new Error(`${label} must be an object.`);
|
|
@@ -616,3 +779,10 @@ function requiredNumber(source, key) {
|
|
|
616
779
|
}
|
|
617
780
|
return value;
|
|
618
781
|
}
|
|
782
|
+
function boundedInteger(source, key, minimum, maximum) {
|
|
783
|
+
const value = requiredNumber(source, key);
|
|
784
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
785
|
+
throw new Error(`${key} must be an integer from ${minimum} through ${maximum}.`);
|
|
786
|
+
}
|
|
787
|
+
return value;
|
|
788
|
+
}
|
package/dist/shared/path/path.js
CHANGED
|
@@ -4,7 +4,9 @@ export function isSubPath(root, candidate) {
|
|
|
4
4
|
const normalizedRoot = path.resolve(root);
|
|
5
5
|
const normalizedCandidate = path.resolve(candidate);
|
|
6
6
|
const relative = path.relative(normalizedRoot, normalizedCandidate);
|
|
7
|
-
return relative === "" || (
|
|
7
|
+
return relative === "" || (relative !== ".."
|
|
8
|
+
&& !relative.startsWith(`..${path.sep}`)
|
|
9
|
+
&& !path.isAbsolute(relative));
|
|
8
10
|
}
|
|
9
11
|
export async function resolveExistingDirectoryPath(inputPath) {
|
|
10
12
|
const stat = await fs.stat(inputPath);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HostProcessSupervisorMetadata } from "./host-process-supervisor.js";
|
|
2
|
+
export type HostProcessRecoveryOutcome = "absent" | "terminated" | "pid_reused" | "unverifiable";
|
|
3
|
+
export interface HostProcessRecovery {
|
|
4
|
+
recover(metadata: HostProcessSupervisorMetadata): Promise<HostProcessRecoveryOutcome>;
|
|
5
|
+
}
|
|
6
|
+
/** Exact, birth-marker-fenced recovery for a process tree left by an old bridge. */
|
|
7
|
+
export declare class ExactHostProcessRecovery implements HostProcessRecovery {
|
|
8
|
+
private readonly platform;
|
|
9
|
+
private readonly graceMs;
|
|
10
|
+
constructor(platform?: NodeJS.Platform, graceMs?: number);
|
|
11
|
+
recover(metadata: HostProcessSupervisorMetadata): Promise<HostProcessRecoveryOutcome>;
|
|
12
|
+
}
|