160 lines
14 KiB
PHP
160 lines
14 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
// New independent finite protocol controls. Real monotonic/wall clocks; local
|
|
// cURL/config stubs only, with literal canonical lifecycle closures.
|
|
namespace think\facade { class Log { public static function warning(...$unused): void {} } }
|
|
namespace app\common\service {
|
|
require __DIR__ . '/fixtures/CancellableCurlMultiStub.php';
|
|
function config(string $unused): array { return ['enable' => true, 'base_url' => 'https://independent.invalid/v1/chat-messages',
|
|
'timeout' => 90, 'models' => ['qwen' => ['name' => 'independent', 'api_key' => 'synthetic-only']]]; }
|
|
function curl_init(): \stdClass { return (object) ['errno' => 0]; }
|
|
function curl_setopt_array(\stdClass $h, array $o): bool { $h->options = $o; return true; }
|
|
function curl_exec(\stdClass $h): bool {
|
|
++$GLOBALS['i26bPosts']; $n = $GLOBALS['i26bPosts']; $GLOBALS['i26bInCurl'] = true;
|
|
try {
|
|
\i26bCheck($GLOBALS['i26bControl']->publicState()['submitted_pending_attempt_count'] === 1,
|
|
'owned pending record precedes actual stub execution');
|
|
\i26bCheck(!$GLOBALS['i26bControl']->canStartAttempt(), 'admission blocks a racing same-request generation');
|
|
($h->options[CURLOPT_HEADERFUNCTION])($h, "HTTP/1.1 200 OK\r\n");
|
|
if ($n > 1) $frames = [['event' => 'workflow_started', 'task_id' => 'synthetic-task', 'workflow_run_id' => 'synthetic-run'],
|
|
['event' => 'message', 'task_id' => 'synthetic-task', 'answer' => "原字节\r\n乙。"],
|
|
['event' => 'workflow_finished', 'task_id' => 'synthetic-task', 'workflow_run_id' => 'synthetic-run', 'data' => ['status' => 'succeeded']],
|
|
['event' => 'message_end']];
|
|
else {
|
|
$error = ['event' => 'error', 'code' => 'invalid_inputs', 'message' => 'synthetic validation rejection'];
|
|
$mode = $GLOBALS['i26bMode'];
|
|
$frames = [];
|
|
if ($mode === 'error_original_run') $error['workflow_run_id'] = 'synthetic-unbound-run';
|
|
if ($mode === 'error_answer') $error['answer'] = 'synthetic execution evidence';
|
|
if ($mode === 'error_outputs') $error['outputs'] = ['answer' => 'synthetic execution evidence'];
|
|
if ($mode === 'error_choices') $error['choices'] = [['delta' => ['content' => 'synthetic execution evidence']]];
|
|
if ($mode === 'upper_terminal') $frames[] = ['event' => 'WORKFLOW_FINISHED', 'task_id' => 'synthetic-unbound-task',
|
|
'workflow_run_id' => 'synthetic-unbound-run', 'data' => ['status' => 'stopped']];
|
|
if ($mode === 'lower_terminal') $frames[] = ['event' => 'workflow_finished', 'task_id' => 'synthetic-unbound-task',
|
|
'workflow_run_id' => 'synthetic-unbound-run', 'data' => ['status' => 'stopped']];
|
|
if ($mode === 'node') $frames[] = ['event' => 'node_started'];
|
|
$frames[] = $mode === 'quiet' ? ['event' => 'message_end'] : $error;
|
|
}
|
|
foreach ($frames as $frame) ($h->options[CURLOPT_WRITEFUNCTION])($h,
|
|
'data: ' . json_encode($frame, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n\n");
|
|
return true;
|
|
} finally { $GLOBALS['i26bInCurl'] = false; }
|
|
}
|
|
function curl_errno(\stdClass $h): int { return 0; }
|
|
function curl_getinfo(\stdClass $h, int $o): int|float { return $o === CURLINFO_HTTP_CODE ? 200 : 0.001; }
|
|
function curl_close(\stdClass $h): void { $GLOBALS['i26bClosed'] = $GLOBALS['i26bPosts']; }
|
|
class DifyWorkflowRunStatus {
|
|
public static function lookup(array $id, int $ms): array {
|
|
++$GLOBALS['i26bGets'];
|
|
\i26bCheck(!$GLOBALS['i26bInCurl'] && $GLOBALS['i26bClosed'] === $GLOBALS['i26bPosts'], 'lookup follows actual handle close');
|
|
\i26bCheck(count($GLOBALS['i26bTiming']) === $GLOBALS['i26bPosts'], 'timing precedes verifier I/O');
|
|
\i26bCheck(array_keys($id) === ['profile', 'user', 'task_id', 'workflow_run_id', 'generation'], 'no private token reaches adapter');
|
|
\i26bCheck($id['task_id'] === 'synthetic-task' && $id['workflow_run_id'] === 'synthetic-run', 'no borrowed unbound metadata reaches adapter');
|
|
\i26bCheck($ms > 0 && $ms <= 1000, 'lookup I/O cap remains finite');
|
|
return ['ok' => true, 'workflow_run_id' => $id['workflow_run_id'], 'status' => 'succeeded'];
|
|
}
|
|
}
|
|
}
|
|
namespace {
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
use app\adminapi\service\AssistantRequestControl as Control;
|
|
use app\common\service\DifyChatService;
|
|
$checks = 0; $failures = []; $observations = []; $number = 0;
|
|
function i26bCheck(bool $ok, string $label): void { ++$GLOBALS['checks']; if (!$ok) $GLOBALS['failures'][] = $label; }
|
|
$root = dirname(__DIR__, 2);
|
|
$pins = ['server/app/adminapi/service/AssistantRequestControl.php' => 'e7a43edad2c4c64ba968d52b991b7f0b3dba12de0165a01062cf9be2bcd3ee7d',
|
|
'server/app/common/service/DifyChatService.php' => 'af60ea942e57e02e712ae2ed8e1ff4724a2c4aa52d253a81f0a4351179ded028',
|
|
'server/app/adminapi/controller/tcm/DiagnosisController.php' => '83d47bf0ffb47c0f06a0cd69db910d28c0e790ccbfb5e5d3e5c931bf84f72568'];
|
|
foreach ($pins as $p => $hash) i26bCheck(hash_file('sha256', $root . '/' . $p) === $hash, 'exact candidate pin ' . $p);
|
|
$dir = sys_get_temp_dir() . '/r26b-independent-submission-' . bin2hex(random_bytes(8));
|
|
$read = static fn (string $p): array => json_decode(file_get_contents($p), true, 64, JSON_THROW_ON_ERROR);
|
|
$write = static fn (string $p, array $s): int|false => file_put_contents($p, json_encode($s, JSON_THROW_ON_ERROR));
|
|
$make = static function () use ($dir, &$number): array {
|
|
$uuid = sprintf('e5678901-1234-4234-8234-%012d', ++$number); $c = new Control(71, 11, $uuid, $dir);
|
|
i26bCheck($c->begin(), 'new isolated control begins'); return [$c, $dir . '/' . hash('sha256', '71:11:' . $uuid) . '.json', $uuid];
|
|
};
|
|
$source = file_get_contents($root . '/server/app/adminapi/controller/tcm/DiagnosisController.php'); $literals = [];
|
|
foreach (['on_submission', 'on_task', 'on_terminal', 'on_transfer_end', 'can_start_attempt', 'on_timeout'] as $key) {
|
|
$pattern = in_array($key, ['on_submission', 'on_task'], true)
|
|
? '/\$options\[\'' . preg_quote($key, '/') . '\'\] = (static function.*?\n });/s'
|
|
: '/\'' . preg_quote($key, '/') . '\' => (static function.*?\n }),/s';
|
|
if (preg_match_all($pattern, $source, $m) !== 1) throw new RuntimeException('Canonical callback not unique');
|
|
$literals[$key] = $m[1][0];
|
|
}
|
|
try {
|
|
foreach (['pure_rejection', 'error_original_run', 'error_answer', 'error_outputs', 'error_choices', 'upper_terminal', 'lower_terminal', 'node', 'quiet'] as $mode) {
|
|
[$control, $path] = $make(); $GLOBALS['i26bControl'] = $control; $GLOBALS['i26bMode'] = $mode;
|
|
$GLOBALS['i26bPosts'] = $GLOBALS['i26bGets'] = $GLOBALS['i26bClosed'] = 0;
|
|
$GLOBALS['i26bInCurl'] = false; $GLOBALS['i26bTiming'] = []; $profile = 'qwen'; $upstreamUser = 'synthetic-user'; $options = [];
|
|
foreach ($literals as $key => $literal) $options[$key] = eval('namespace app\\adminapi\\controller\\tcm; use app\\common\\service\\DifyWorkflowRunStatus; return ' . $literal . ';');
|
|
$options['on_upstream_timing'] = static function ($timing): void { $GLOBALS['i26bTiming'][] = $timing->snapshot(); };
|
|
$options['knowledge_mode'] = 'evidence'; $options['timeout_ms'] = 90000; $options['request_deadline_ns'] = hrtime(true) + 90000000000;
|
|
$clinical = '';
|
|
$out = DifyChatService::streamChat('qwen', ['fixture' => 'input'], 'synthetic-query', 'synthetic-user',
|
|
static function (string $text) use (&$clinical): bool { $clinical .= $text; return true; }, static fn (): bool => false, [], $options);
|
|
$p = $control->publicState(); $s = $read($path); $first = array_values($s['submitted_attempts'])[0];
|
|
$observations[] = ['case' => $mode, 'posts' => $GLOBALS['i26bPosts'], 'gets' => $GLOBALS['i26bGets'],
|
|
'pending' => $p['upstream_pending_task_count'], 'rejected_before_execution' => $p['rejected_before_execution_attempt_count'],
|
|
'can_start' => $control->canStartAttempt(), 'first_disposition' => $first['disposition']];
|
|
if ($mode === 'pure_rejection') {
|
|
i26bCheck($GLOBALS['i26bPosts'] === 2 && $GLOBALS['i26bGets'] === 1 && !empty($out['ok']), 'plain finite SSE input rejection retains compatibility');
|
|
i26bCheck($clinical === "原字节\r\n乙。", 'positive compatibility preserves clinical bytes');
|
|
} else {
|
|
i26bCheck($GLOBALS['i26bPosts'] === 1 && $GLOBALS['i26bGets'] === 0, 'no successor or guessed GET after execution evidence ' . $mode);
|
|
i26bCheck($p['upstream_pending_task_count'] === 1 && !$control->canStartAttempt(), 'execution evidence remains pending ' . $mode);
|
|
i26bCheck($first['disposition'] === 'unconfirmed' && $p['rejected_before_execution_attempt_count'] === 0, 'execution evidence cannot become pre-execution rejection ' . $mode);
|
|
}
|
|
$encoded = json_encode($p, JSON_THROW_ON_ERROR);
|
|
foreach (['synthetic-task', 'synthetic-run', 'synthetic-user', 'synthetic-query', 'synthetic-only', $first === [] ? '' : array_key_first($s['submitted_attempts'])] as $secret)
|
|
i26bCheck($secret === '' || !str_contains($encoded, $secret), 'public state excludes synthetic identity/input/token');
|
|
}
|
|
|
|
// Real clock-domain checks: admission/observations are epoch wall time;
|
|
// persisted budget origins are host-monotonic and not comparable to it.
|
|
[$c, $path] = $make(); $wallBefore = microtime(true); $monoBefore = hrtime(true) / 1e9;
|
|
$token = $c->beginSubmission('qwen', 'clock-user'); $c->bindTask('qwen', 'clock-user', 'clock-task', 'clock-run', $token);
|
|
$c->markTerminal('clock-task', 'stopped', 'clock-run'); $gets = 0;
|
|
$r = $c->closeSubmittedGeneration($token, 'clock-task', 'unconfirmed', static function (array $id, int $ms) use (&$gets, $dir, $path): array {
|
|
++$gets;
|
|
foreach ([$dir . '/.gc.lock', $path] as $file) { $h = fopen($file, 'c+'); $ok = flock($h, LOCK_EX | LOCK_NB);
|
|
i26bCheck($ok, 'no GC/request lock across actual injected lookup'); if ($ok) flock($h, LOCK_UN); fclose($h); }
|
|
return ['ok' => true, 'workflow_run_id' => $id['workflow_run_id'], 'status' => 'failed'];
|
|
});
|
|
$wallAfter = microtime(true); $monoAfter = hrtime(true) / 1e9; $s = $read($path); $a = $s['completed_generations'][1];
|
|
foreach ([$s['submitted_attempts'][$token]['submitted_at'], $s['submitted_attempts'][$token]['transfer_closed_at'],
|
|
$a['producer_terminal_observed_at'], $a['persisted_run_observed_at'], $a['transfer_closed_at'], $a['run_status_observations'][0]['observed_at']] as $time)
|
|
i26bCheck($time >= $wallBefore && $time <= $wallAfter, 'observation remains actual wall clock');
|
|
i26bCheck($a['verification_started_at'] >= $monoBefore && $a['verification_started_at'] <= $monoAfter, 'verification budget origin remains actual monotonic clock');
|
|
i26bCheck($r['confirmed'] && $gets === 1 && $c->canStartAttempt() && $c->publicState()['lifecycle_conflict'], 'exact differing terminal enum resolves with retained conflict');
|
|
i26bCheck($a['producer_terminal_status'] === 'stopped' && $a['persisted_run_status'] === 'failed', 'both actual status observations retained');
|
|
$observations[] = ['case' => 'real_clock_domains', 'wall_observations_in_wall_interval' => true,
|
|
'monotonic_budget_origin_in_monotonic_interval' => true, 'wall_monotonic_gap_exceeds_one_day' => abs($wallAfter - $monoAfter) > 86400];
|
|
|
|
// Unknown work survives expiry, cross-worker reads, stale EOF and a new
|
|
// unrelated UUID; explicit pre-execution resolution cannot rewrite it.
|
|
[$c, $path, $uuid] = $make(); $token = $c->beginSubmission('qwen', 'isolated-user');
|
|
$never = static function (): array { i26bCheck(false, 'no lookup without original pair'); return []; };
|
|
$c->closeSubmittedGeneration($token, '', 'unconfirmed', $never);
|
|
$c->closeSubmittedGeneration($token, '', 'rejected_before_execution', $never);
|
|
i26bCheck($c->publicState()['submitted_without_identity_count'] === 1 && !$c->canStartAttempt(), 'late EOF cannot reclassify unknown submission');
|
|
$s = $read($path); $s['expires_at'] = time() - 2000; $write($path, $s); touch($path, time() - 2000);
|
|
$peer = new Control(71, 11, $uuid, $dir);
|
|
i26bCheck(is_file($path) && !$peer->begin() && $peer->publicState()['upstream_pending_task_count'] === 1, 'expired unknown attempt survives GC and cross-worker replay');
|
|
[$other] = $make(); i26bCheck($other->canStartAttempt(), 'unrelated UUID has no global cleanup gate');
|
|
$foreign = $other->closeSubmittedGeneration($token, '', 'not_submitted', $never);
|
|
i26bCheck($foreign['reason'] === 'stale_submission' && $peer->publicState()['submitted_without_identity_count'] === 1, 'foreign EOF cannot affect original ownership');
|
|
foreach ($pins as $p => $hash) i26bCheck(hash_file('sha256', $root . '/' . $p) === $hash, 'exact candidate pin after ' . $p);
|
|
echo json_encode(['scope' => 'new independent r26b production finite submission controls', 'assertions' => $checks,
|
|
'failures' => $failures, 'observations' => $observations, 'real_network_calls' => 0, 'real_model_calls' => 0], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n";
|
|
} finally {
|
|
$real = realpath($dir); $temp = realpath(sys_get_temp_dir());
|
|
if ($real !== false) {
|
|
if ($temp === false || !str_starts_with(str_replace('\\', '/', $real), rtrim(str_replace('\\', '/', $temp), '/') . '/r26b-independent-submission-')) throw new RuntimeException('Cleanup boundary');
|
|
foreach (new DirectoryIterator($real) as $f) { if ($f->isDot()) continue; if ($f->isLink() || !$f->isFile()) throw new RuntimeException('Unexpected temp entry'); unlink($f->getPathname()); }
|
|
rmdir($real);
|
|
}
|
|
}
|
|
exit($failures === [] ? 0 : 1);
|
|
}
|