194 lines
16 KiB
PHP
194 lines
16 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
// Independent, in-process GET/config/clock stubs and handcrafted audit fixtures.
|
||
// Do not load the probe, application, original credentials or runtime audits.
|
||
namespace PrivateTcmR25 {
|
||
function hrtime(bool $number = false): int { return $GLOBALS['iiClock']; }
|
||
function curl_init(): object {
|
||
++$GLOBALS['iiCreated'];
|
||
$scenario = array_shift($GLOBALS['iiScenarios']);
|
||
if (!is_array($scenario)) throw new \RuntimeException('Unexpected synthetic GET');
|
||
return (object) ['scenario' => $scenario, 'options' => [], 'errno' => 0];
|
||
}
|
||
function curl_setopt_array(object $handle, array $options): bool {
|
||
$handle->options = $options; $GLOBALS['iiOptions'][] = $options;
|
||
$GLOBALS['iiClock'] += $handle->scenario['setup_ns'] ?? 0;
|
||
return ($handle->scenario['set_ok'] ?? true) === true;
|
||
}
|
||
function curl_exec(object $handle): bool {
|
||
++$GLOBALS['iiExecuted'];
|
||
$GLOBALS['iiClock'] += $handle->scenario['advance_ns'] ?? 0;
|
||
if ($handle->scenario['throw'] ?? false) throw new \RuntimeException('private-independent-provider-error');
|
||
foreach ($handle->scenario['chunks'] ?? [$handle->scenario['body']] as $chunk) {
|
||
if (($handle->options[CURLOPT_WRITEFUNCTION])($handle, $chunk) !== strlen($chunk)) {
|
||
$handle->errno = CURLE_WRITE_ERROR; return false;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
function curl_errno(object $handle): int { return $handle->errno; }
|
||
function curl_getinfo(object $handle, int $option): int { return $handle->scenario['status'] ?? 200; }
|
||
function curl_close(object $handle): void { ++$GLOBALS['iiClosed']; }
|
||
}
|
||
namespace {
|
||
require dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/private_tcm_r25_run_input_inspector.php';
|
||
use PrivateTcmR25\RunInputInspector as Inspector;
|
||
|
||
$checks = 0; $iiClock = 10000000000; $iiCreated = 0; $iiClosed = 0; $iiExecuted = 0; $iiScenarios = []; $iiOptions = [];
|
||
$expect = static function (bool $value, string $label) use (&$checks): void { ++$checks; if (!$value) throw new RuntimeException($label); };
|
||
$json = static fn (mixed $value): string => json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||
$base = sys_get_temp_dir() . '/tcm-r25-inspector-independent-' . bin2hex(random_bytes(8));
|
||
$root = $base . '/server'; $auditDir = $root . '/runtime/tcm-r25-wire-audits'; $resultDir = $base . '/app/artifacts/ai_concurrency';
|
||
mkdir($auditDir, 0700, true); mkdir($resultDir, 0700, true);
|
||
$number = 0;
|
||
$query = "independent-private-query/中文\nsecond-line\r\n";
|
||
$config = ['enable' => true, 'base_url' => 'http://independent.synthetic.invalid:8088/v1/chat-messages',
|
||
'models' => ['qwen' => ['api_key' => 'private-independent-qwen-key']]];
|
||
$fixtureHash = 'b54c47ec11f6a99dc87bdda78629e18c8a6f37a64017e42a72db399c727cb1c6';
|
||
$questionHash = '28fbaee8585d4fbde3be3b1a6ee046c1a60f18a1fe67c0769211b08afdd5d306';
|
||
$make = static function (int $count = 1) use (&$number, $query, $fixtureHash, $questionHash): array {
|
||
$id = sprintf('44444444-4444-4444-8444-%012d', ++$number);
|
||
$records = [['event' => 'request', 'contract' => 'v43r25_private_run_wire', 'request_id' => $id,
|
||
'request_label' => 'independent_row_qwen', 'profile' => 'qwen']];
|
||
for ($i = 1; $i <= $count; ++$i) {
|
||
$body = json_encode(['inputs' => (object) [], 'query' => $query], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||
$records[] = ['event' => 'serialized_request', 'generation' => $i, 'report_stage' => $i === 1 ? 1 : 2,
|
||
'stage_attempt' => $i <= 2 ? 1 : $i - 1, 'protocol' => 'dify', 'body_sha256' => hash('sha256', $body), 'body_bytes' => strlen($body),
|
||
'query_sha256' => hash('sha256', $query), 'query_bytes' => strlen($query),
|
||
'inputs_sha256' => hash('sha256', '{}'), 'inputs_bytes' => 2, 'report_continuation_query' => $i > 1];
|
||
$records[] = ['event' => 'original_identity', 'generation' => $i, 'original_task_id' => 'private-independent-task-' . $i, 'original_workflow_run_id' => ''];
|
||
$records[] = ['event' => 'original_identity', 'generation' => $i, 'original_task_id' => 'private-independent-task-' . $i, 'original_workflow_run_id' => 'private-independent-run-' . $i];
|
||
}
|
||
$records[] = ['event' => 'after_control_cleanup', 'state' => ['transport_closed' => true, 'upstream_terminal_confirmed' => true,
|
||
'upstream_stop_confirmed' => false, 'cancel_requested' => false, 'timeout_reached' => false, 'upstream_pending_task_count' => 0]];
|
||
return ['records' => $records, 'summary' => ['persisted' => true, 'serialized_request_count' => $count,
|
||
'original_task_count' => $count, 'original_run_count' => $count], 'id' => $id, 'number' => $number,
|
||
'fixture' => $fixtureHash, 'question' => $questionHash];
|
||
};
|
||
$save = static function (array $fixture) use ($auditDir, $resultDir, $json): array {
|
||
$content = implode("\n", array_map($json, $fixture['records'])) . "\n";
|
||
$summary = $fixture['summary'] + ['audit_bytes' => strlen($content), 'audit_sha256' => hash('sha256', $content)];
|
||
$auditPath = $auditDir . '/' . hash('sha256', $fixture['id']) . '.private.jsonl';
|
||
file_put_contents($auditPath, $content);
|
||
$row = ['test_label' => 'independent_row', 'profile' => 'qwen', 'private_wire_audit' => $summary,
|
||
'harness_contract' => 'v43r25_private_run_wire', 'synthetic' => true, 'preparation_isolated' => true,
|
||
'preparation_isolation_verified' => true, 'production_acceptance_path' => true, 'diagnostic_only' => false, 'case_id' => 11,
|
||
'production_hashes' => ['fixture' => $fixture['fixture'], 'question' => $fixture['question']]];
|
||
$document = ['harness_contract' => 'v43r25_private_run_wire', 'synthetic_only' => true, 'preparation_isolated' => true,
|
||
'fixture_sha256' => $fixture['fixture'], 'canonical_question_sha256' => $fixture['question'], 'rows' => [$row]];
|
||
$resultFile = 'tcm_complete_independent_inspector_' . $fixture['number'] . '.safe.json';
|
||
$resultPath = $resultDir . '/' . $resultFile; file_put_contents($resultPath, $json($document));
|
||
return [['result_file' => $resultFile, 'test_label' => 'independent_row', 'profile' => 'qwen', 'audit_sha256' => $summary['audit_sha256']], $auditPath, $resultPath];
|
||
};
|
||
$response = static fn (int $i = 1): array => ['id' => 'private-independent-run-' . $i, 'status' => 'succeeded',
|
||
'inputs' => ['sys.query' => $query], 'outputs' => ['secret' => 'private-independent-output']];
|
||
$call = static function (array $selector, array $scenarios, ?array $override = null) use ($root, $config): array {
|
||
$GLOBALS['iiScenarios'] = $scenarios; $configCalls = 0;
|
||
$value = Inspector::inspectResultRow($root, $selector, static function () use (&$configCalls, $config, $override): array { ++$configCalls; return $override ?? $config; });
|
||
return [$value, $configCalls];
|
||
};
|
||
$privacy = static function (array $value) use ($expect, $json, $query): void {
|
||
$text = $json($value);
|
||
foreach (['private-independent-', 'independent.synthetic', 'independent_row', '44444444-', $query] as $secret)
|
||
$expect(!str_contains($text, $secret), 'safe reply excludes private identity/body/config/selector text');
|
||
$expect(($value['model_calls'] ?? null) === 0 && ($value['llm_expanded_prompt_observed'] ?? null) === false, 'query evidence never becomes model/node visibility');
|
||
};
|
||
try {
|
||
[$selected, , $resultPath] = $save($make(2));
|
||
[$ok, $configured] = $call($selected, [['body' => $json($response())], ['body' => $json($response(2))]]);
|
||
$expect($ok['ok'] && $configured === 1 && $ok['inspected_run_count'] === 2 && $ok['matched_run_count'] === 2, 'full saved-row bridge selects exactly two original runs');
|
||
$expect($ok['result_file_sha256'] === hash_file('sha256', $resultPath) && $ok['audit_sha256'] === $selected['audit_sha256'], 'whole result and audit digests retained');
|
||
$privacy($ok);
|
||
foreach ($iiOptions as $options) {
|
||
$expect($options[CURLOPT_HTTPGET] === true && !isset($options[CURLOPT_POSTFIELDS]) && !isset($options[CURLOPT_POST]), 'GET only');
|
||
$expect($options[CURLOPT_PROTOCOLS] === CURLPROTO_HTTP && $options[CURLOPT_REDIR_PROTOCOLS] === CURLPROTO_HTTP
|
||
&& str_starts_with($options[CURLOPT_URL], 'http://independent.synthetic.invalid:8088/v1/workflows/run/'), 'exact configured HTTP scheme/host/port/path only');
|
||
$expect($options[CURLOPT_TIMEOUT_MS] > 0 && $options[CURLOPT_TIMEOUT_MS] <= 2000 && $options[CURLOPT_CONNECTTIMEOUT_MS] <= 2000
|
||
&& !$options[CURLOPT_FOLLOWLOCATION] && $options[CURLOPT_MAXREDIRS] === 0, 'bounded deadline and redirects disabled');
|
||
}
|
||
foreach (['open_transport', 'no_terminal', 'pending', 'pending_string', 'extra_footer', 'header_upper_uuid', 'header_neighbor_label',
|
||
'identity_downgrade', 'identity_other_task', 'identity_before_serialization', 'wrong_query_hash_type', 'stage_backwards', 'non_dify',
|
||
'summary_bool_count', 'summary_byte_mismatch', 'summary_run_mismatch', 'too_many_runs'] as $kind) {
|
||
$fixture = $make(in_array($kind, ['stage_backwards', 'too_many_runs'], true) ? 3 : 1);
|
||
$last = count($fixture['records']) - 1;
|
||
if ($kind === 'open_transport') $fixture['records'][$last]['state']['transport_closed'] = false;
|
||
if ($kind === 'no_terminal') $fixture['records'][$last]['state']['upstream_terminal_confirmed'] = false;
|
||
if ($kind === 'pending') $fixture['records'][$last]['state']['upstream_pending_task_count'] = 1;
|
||
if ($kind === 'pending_string') $fixture['records'][$last]['state']['upstream_pending_task_count'] = '0';
|
||
if ($kind === 'extra_footer') $fixture['records'][$last]['state']['raw_body'] = 'private-independent-injection';
|
||
if ($kind === 'header_upper_uuid') $fixture['records'][0]['request_id'] = strtoupper(str_replace('44444444', 'abcdefab', $fixture['id']));
|
||
if ($kind === 'header_neighbor_label') $fixture['records'][0]['request_label'] = 'neighbor_qwen';
|
||
if ($kind === 'identity_downgrade') $fixture['records'][3]['original_workflow_run_id'] = '';
|
||
if ($kind === 'identity_other_task') $fixture['records'][3]['original_task_id'] = 'private-independent-other-task';
|
||
if ($kind === 'identity_before_serialization') [$fixture['records'][1], $fixture['records'][2]] = [$fixture['records'][2], $fixture['records'][1]];
|
||
if ($kind === 'wrong_query_hash_type') $fixture['records'][1]['query_sha256'] = ['private-independent-injection'];
|
||
if ($kind === 'stage_backwards') { $fixture['records'][7]['report_stage'] = 1; $fixture['records'][7]['report_continuation_query'] = false; }
|
||
if ($kind === 'non_dify') $fixture['records'][1]['protocol'] = 'openai';
|
||
if ($kind === 'summary_bool_count') $fixture['summary']['original_task_count'] = true;
|
||
if ($kind === 'summary_byte_mismatch') $fixture['summary']['audit_bytes'] = 1;
|
||
if ($kind === 'summary_run_mismatch') $fixture['summary']['original_run_count'] = 0;
|
||
[$selector] = $save($fixture); $before = $iiExecuted;
|
||
[$value, $configured] = $call($selector, []);
|
||
$expect(!$value['ok'] && $configured === 0 && $iiExecuted === $before, 'invalid exact-own bridge fails before config/GET: ' . $kind);
|
||
$privacy($value);
|
||
}
|
||
foreach (['object_sys', 'lookalike_dot', 'trailing_space', 'wrong_case', 'array_inputs', 'null_query', 'different_crlf',
|
||
'different_unicode', 'wrong_byte_count', 'optional_null_run', 'optional_null_task', 'output_only', 'both_same', 'running_input_only'] as $kind) {
|
||
$fixture = $make(); $value = $response();
|
||
if ($kind === 'object_sys') $value['inputs'] = ['sys' => ['query' => $query]];
|
||
if ($kind === 'lookalike_dot') $value['inputs'] = ['sys.query' => $query];
|
||
if ($kind === 'trailing_space') $value['inputs'] = ['sys.query ' => $query];
|
||
if ($kind === 'wrong_case') $value['inputs'] = ['SYS.QUERY' => $query];
|
||
if ($kind === 'array_inputs') $value['inputs'] = [['sys.query' => $query]];
|
||
if ($kind === 'null_query') $value['inputs']['query'] = null;
|
||
if ($kind === 'different_crlf') $value['inputs']['sys.query'] = str_replace("\r\n", "\n", $query);
|
||
if ($kind === 'different_unicode') $value['inputs']['sys.query'] .= "\u{0301}";
|
||
if ($kind === 'wrong_byte_count') ++$fixture['records'][1]['query_bytes'];
|
||
if ($kind === 'optional_null_run') $value['workflow_run_id'] = null;
|
||
if ($kind === 'optional_null_task') $value['task_id'] = null;
|
||
if ($kind === 'output_only') { $value['inputs'] = (object) []; $value['outputs']['query'] = $query; }
|
||
if ($kind === 'both_same') $value['inputs']['query'] = $query;
|
||
if ($kind === 'running_input_only') $value['status'] = 'running';
|
||
[$selector] = $save($fixture); $before = $iiExecuted;
|
||
[$answer] = $call($selector, [['body' => $json($value)]]);
|
||
$expect($answer['ok'] === in_array($kind, ['both_same', 'running_input_only'], true) && $iiExecuted === $before + 1,
|
||
'only exact whitelisted byte-identical query evidence: ' . $kind);
|
||
if ($kind === 'running_input_only') $expect($answer['runs'][0]['run_status'] === 'running', 'matching query does not assert a fresh terminal or cleanup proof');
|
||
$privacy($answer);
|
||
}
|
||
[$selector] = $save($make(2)); $before = $iiExecuted;
|
||
[$answer] = $call($selector, [['body' => $json($response()), 'advance_ns' => 2000000000], ['body' => $json($response(2))]]);
|
||
$expect(!$answer['ok'] && $iiExecuted === $before + 2 && $answer['matched_run_count'] === 1
|
||
&& $answer['runs'][0]['status'] === 'run_timeout' && $answer['runs'][1]['query_matches_wire'], 'timeout plus successor uses exactly two original GETs and cannot hide first failure');
|
||
$privacy($answer);
|
||
foreach (['setup_deadline', 'setopt_failure', 'chunk_overflow', 'throw'] as $kind) {
|
||
[$selector] = $save($make()); $scenario = ['body' => $json($response())];
|
||
if ($kind === 'setup_deadline') $scenario['setup_ns'] = 2000000000;
|
||
if ($kind === 'setopt_failure') $scenario['set_ok'] = false;
|
||
if ($kind === 'chunk_overflow') $scenario['chunks'] = [str_repeat('x', 1048576), str_repeat('x', 1048576), 'x'];
|
||
if ($kind === 'throw') $scenario['throw'] = true;
|
||
$before = $iiExecuted; [$answer] = $call($selector, [$scenario]);
|
||
$expect(!$answer['ok'] && $iiExecuted === $before + (in_array($kind, ['setup_deadline', 'setopt_failure'], true) ? 0 : 1), 'bounded setup/write/error closes without retry: ' . $kind);
|
||
$privacy($answer);
|
||
}
|
||
[$selector, $auditPath] = $save($make());
|
||
$lock = fopen($auditPath, 'rb');
|
||
$expect(flock($lock, LOCK_EX | LOCK_NB), 'synthetic exclusive lock acquired');
|
||
$before = $iiExecuted; [$answer, $configured] = $call($selector, []);
|
||
$expect(!$answer['ok'] && $configured === 0 && $iiExecuted === $before, 'locked exact private audit never reaches config/GET');
|
||
flock($lock, LOCK_UN); fclose($lock);
|
||
$expect($iiCreated === $iiClosed, 'every fake GET handle closed');
|
||
$expect($iiScenarios === [], 'all and only declared fake scenarios consumed');
|
||
$expect(!class_exists('think\\App', false), 'application bootstrap never loaded');
|
||
echo "Independent exact-run input inspector: {$checks} checks passed (handcrafted private fixtures, in-memory GET/config/time; no auth/network/bootstrap).\n";
|
||
} finally {
|
||
$real = realpath($base); $temp = realpath(sys_get_temp_dir());
|
||
if ($real === false || $temp === false || !str_starts_with(str_replace('\\', '/', $real), rtrim(str_replace('\\', '/', $temp), '/') . '/tcm-r25-inspector-independent-'))
|
||
throw new RuntimeException('Independent synthetic cleanup boundary');
|
||
$entries = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($real, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST);
|
||
foreach ($entries as $entry) { if ($entry->isLink()) throw new RuntimeException('Independent synthetic cleanup symlink'); if ($entry->isDir()) rmdir($entry->getPathname()); else unlink($entry->getPathname()); }
|
||
rmdir($real);
|
||
}
|
||
}
|