Skip to content

Commit 28e2fe2

Browse files
1625567290M4n5ter
authored andcommitted
限制托管执行回执并分离超时输出
1 parent 61e1c69 commit 28e2fe2

4 files changed

Lines changed: 93 additions & 4 deletions

File tree

packages/eval/harbor/relay_agent.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,10 @@ async def _prepare_command(
393393
if secret_path is not None:
394394
secret_path.unlink(missing_ok=True)
395395
subject = shlex.join([request["command"], *request["args"]])
396-
output_redirect = "" if capture_stdout else " >/dev/null"
396+
# An exit-code subject has no structured stdout to preserve. Detach both
397+
# streams so a task-owned background process cannot keep Harbor's
398+
# `docker compose exec` output pipe open after the subject leader exits.
399+
output_redirect = "" if capture_stdout else " >/dev/null 2>&1"
397400
scope_error = shlex.quote(f"{SCOPE_ERROR_PREFIX} {result_token}\\n")
398401
inner = (
399402
"umask 077; "

packages/eval/harbor/test_relay_lifecycle.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,23 @@ async def trial(*_args):
433433

434434

435435
class RelayLifecycleTest(unittest.IsolatedAsyncioTestCase):
436+
async def test_exit_code_subject_detaches_both_output_streams(self):
437+
relay = load_relay()
438+
command = await relay._prepare_command(
439+
ExplodingSubjectEnvironment(),
440+
{
441+
"command": "/bin/sh",
442+
"args": ["-c", "run-subject"],
443+
"credentials": {},
444+
"resultToken": "0" * 32,
445+
"captureStdout": False,
446+
},
447+
"detach-output",
448+
"/tmp/maka-eval-detach-output.pid",
449+
)
450+
451+
self.assertIn(">/dev/null 2>&1", command)
452+
436453
async def test_framework_timeout_uses_its_own_budget_and_completes_the_relay(self):
437454
relay = load_relay()
438455
environment = FrameworkBudgetEnvironment()

packages/runtime-host/src/__tests__/hosted-execution-coordinator.test.ts

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,63 @@ test('fast settlement remains cached and cannot execute twice', async () => {
162162
assert.equal(runs, 1);
163163
});
164164

165+
test('one-shot Host rejects every different execution identity and retains one receipt', async () => {
166+
let runs = 0;
167+
let aborts = 0;
168+
const coordinator = new HostHostedExecutionCoordinator(
169+
async (execution, signal) => {
170+
runs += 1;
171+
signal.addEventListener('abort', () => {
172+
aborts += 1;
173+
});
174+
return settled(execution.executionId, 'completed');
175+
},
176+
() => {},
177+
);
178+
const admitted = await coordinator.handlers['hosted.execution.admit'](input(), context());
179+
assert.equal(admitted.ok, true);
180+
181+
const outcomes = await Promise.all(
182+
Array.from({ length: 200 }, (_, index) =>
183+
coordinator.handlers['hosted.execution.admit'](
184+
input(`00000000-0000-4000-8001-${String(index).padStart(12, '0')}`),
185+
context(),
186+
),
187+
),
188+
);
189+
190+
assert.equal(
191+
outcomes.every((outcome) => !outcome.ok),
192+
true,
193+
);
194+
for (const outcome of outcomes) {
195+
if (!outcome.ok) assert.equal(outcome.error.code, 'operation_conflict');
196+
}
197+
assert.equal(runs, 1);
198+
assert.equal(aborts, 0);
199+
coordinator.beginDrain();
200+
assert.equal(aborts, 1);
201+
});
202+
203+
test('cancelling first binds the one-shot Host to that execution identity', async () => {
204+
const coordinator = new HostHostedExecutionCoordinator(
205+
async (execution) => settled(execution.executionId, 'completed'),
206+
() => {},
207+
);
208+
const cancelled = await coordinator.handlers['hosted.execution.cancel'](
209+
{ executionId: ID },
210+
context(),
211+
);
212+
assert.equal(cancelled.ok, true);
213+
214+
const different = await coordinator.handlers['hosted.execution.admit'](
215+
input('00000000-0000-4000-8000-000000000002'),
216+
context(),
217+
);
218+
assert.equal(different.ok, false);
219+
if (!different.ok) assert.equal(different.error.code, 'operation_conflict');
220+
});
221+
165222
test('admission authority cannot cross connection or Host epoch', async () => {
166223
const coordinator = new HostHostedExecutionCoordinator(
167224
async (execution) => settled(execution.executionId, 'completed'),
@@ -208,9 +265,9 @@ function settled(executionId: string, status: 'completed' | 'failed') {
208265
};
209266
}
210267

211-
function input() {
268+
function input(executionId = ID) {
212269
return {
213-
executionId: ID,
270+
executionId,
214271
session: {
215272
workspace: { kind: 'host_path' as const, path: '/workspace' },
216273
modelTarget: { kind: 'explicit' as const, connectionSlug: 'env-openai', model: 'model' },

packages/runtime-host/src/server/hosted-execution-coordinator.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export class HostHostedExecutionCoordinator {
5252

5353
readonly #executions = new Map<string, HostedExecutionRecord>();
5454
readonly #cancelled = new Set<string>();
55+
#executionId: string | undefined;
5556
#accepting = true;
5657

5758
constructor(
@@ -76,6 +77,9 @@ export class HostHostedExecutionCoordinator {
7677
input: HostedExecutionStartInput,
7778
context: ConnectionContext,
7879
): Promise<OperationOutcome<'hosted.execution.admit'>> {
80+
if (this.#executionId !== undefined && this.#executionId !== input.executionId) {
81+
return conflict();
82+
}
7983
if (this.#cancelled.has(input.executionId)) {
8084
return {
8185
ok: false,
@@ -105,6 +109,7 @@ export class HostHostedExecutionCoordinator {
105109
};
106110
}
107111

112+
this.#executionId = input.executionId;
108113
const execution = this.#createExecution(input, context);
109114
return {
110115
ok: true,
@@ -116,6 +121,9 @@ export class HostHostedExecutionCoordinator {
116121
input: HostedExecutionAdmittedStartInput,
117122
context: ConnectionContext,
118123
): Promise<OperationOutcome<'hosted.execution.start'>> {
124+
if (this.#executionId !== undefined && this.#executionId !== input.execution.executionId) {
125+
return conflict();
126+
}
119127
if (this.#cancelled.has(input.execution.executionId)) {
120128
this.requestDrain();
121129
return {
@@ -172,6 +180,10 @@ export class HostHostedExecutionCoordinator {
172180
async #cancel(
173181
input: HostedExecutionReferenceInput,
174182
): Promise<OperationOutcome<'hosted.execution.cancel'>> {
183+
if (this.#executionId !== undefined && this.#executionId !== input.executionId) {
184+
return conflict();
185+
}
186+
this.#executionId = input.executionId;
175187
this.#cancelled.add(input.executionId);
176188
const execution = this.#executions.get(input.executionId);
177189
if (!execution) {
@@ -198,7 +210,7 @@ function sameAuthority(
198210
}
199211

200212
function conflict<
201-
K extends 'hosted.execution.admit' | 'hosted.execution.start',
213+
K extends 'hosted.execution.admit' | 'hosted.execution.start' | 'hosted.execution.cancel',
202214
>(): OperationOutcome<K> {
203215
return {
204216
ok: false,

0 commit comments

Comments
 (0)