) { if (preg_match('/\\A\\$[A-Za-z_][A-Za-z0-9_]*\\$|\\A\\$\\$/',substr($sql,$i),$m)) { $dollar=$m[0]; $i+=strlen($dollar)-1; continue; } } if (!$single && !$double && $ch===';') { if (trim(substr($sql,$i+1))!=='') return false; } } return !$single && !$double && $dollar===''; } function factory_toolDBStatementKeyword($sql='') { $sql=ltrim((string)$sql); while (preg_match('/\\A(?:--[^\\n]*(?:\\n|$)|\\/\\*[\\s\\S]*?\\*\\/\\s*)/',$sql,$m)) { $sql=ltrim(substr($sql,strlen($m[0]))); } if (!preg_match('/\\A([A-Za-z]+)/',$sql,$m)) return ''; return strtoupper($m[1]); } function factory_toolDBRiskForSQL($sql='',$default=2) { $u=strtoupper(preg_replace('/\\s+/',' ',trim((string)$sql))); $first=$this->factory_toolDBStatementKeyword($sql); if ($first==='SELECT' || $first==='WITH' || $first==='EXPLAIN' || $first==='SHOW' || $first==='VALUES') return 0; if ($first==='INSERT') return 1; if ($first==='UPDATE') return strpos($u,' WHERE ')===false ? 2 : 1; if ($first==='DELETE') return strpos($u,' WHERE ')===false ? 3 : 2; if ($first==='CREATE') { if (preg_match('/^CREATE\\s+(TEMP(?:ORARY)?\\s+)?(TABLE|INDEX)\\b/i',$u)) return 1; return 2; } if ($first==='ALTER') { if (preg_match('/^ALTER\\s+TABLE\\s+.+\\s+ADD\\s+COLUMN\\b/i',$u) && !preg_match('/\\bNOT\\s+NULL\\b/i',$u)) return 1; if (preg_match('/^ALTER\\s+TABLE\\s+.+\\s+ADD\\s+(CONSTRAINT|INDEX)\\b/i',$u)) return 1; if (preg_match('/^ALTER\\s+(ROLE|USER|SYSTEM)\\b/i',$u)) return 3; return 2; } if (in_array($first,array('DROP','TRUNCATE','GRANT','REVOKE'),true)) return 3; if ($first==='COMMENT' || $first==='REINDEX' || $first==='ANALYZE') return 1; if ($first==='VACUUM') return 2; return max(2,min(3,(int)$default)); } function factory_toolDBPreflight($Name='',$Arguments=false,$Context=false,$Capability=false) { $ret=(object)array( 'OK'=>false, 'Arguments'=>is_object($Arguments)?clone $Arguments:new stdClass, 'Capability'=>$Capability ); $name=trim((string)$Name); $args=$ret->Arguments; $cap=$Capability && is_object($Capability)?clone $Capability:new stdClass; if (isset($args->table) && $args->table!==null && trim((string)$args->table)!=='') { if ($this->factory_toolDBIdentifier($args->table)===false) { $ret->Error='db_table_identifier_invalid'; return $ret; } } foreach(array('field','key_name','order_by') as $k) { if (isset($args->$k) && $args->$k!==null && trim((string)$args->$k)!=='') { if ($this->factory_toolDBIdentifier($args->$k)===false) { $ret->Error='db_identifier_invalid:'.$k; return $ret; } } } if ($name==='db_read') { $cap->RiskLevel=0; $cap->ReadOnly=1; $args->limit=max(1,min(200,(int)($args->limit??50))); $args->max_bytes=max(1024,min(262144,(int)($args->max_bytes??131072))); if (($args->mode??'')==='sql') { $sql=trim((string)($args->sql??'')); if (!$this->factory_toolDBSingleStatement($sql)) { $ret->Error='db_sql_single_statement_required'; return $ret; } if ($this->factory_toolDBRiskForSQL($sql,3)!==0) { $ret->Error='db_read_sql_not_read_only'; return $ret; } } else if (!isset($args->table) || trim((string)$args->table==='') { $ret->Error='db_table_required'; return $ret; } } else if ($name==='db_write') { $op=strtolower(trim((string)($args->operation??''))); $cap->RiskLevel=$op==='delete'?2:1; if ($this->factory_toolDBProtectedTable($args->table??'')) $cap->RiskLevel=max(2,(int)$cap->RiskLevel); $args->expected_max_rows=max(1,min(1000,(int)($args->expected_max_rows??1))); if (($op==='update' || $op==='upsert' || $op==='delete') && (trim((string)($args->key_name??''))==='' || !property_exists($args,'key_value') || $args->key_value===null)) { $ret->Error='db_key_required'; return $ret; } if (($op==='insert' || $op==='update' || $op==='upsert') && !is_array($args->values)) { $ret->Error='db_values_required'; return $ret; } } else if ($name==='db_schema') { $op=strtolower(trim((string)($args->operation??''))); $args->max_bytes=max(1024,min(262144,(int)($args->max_bytes??131072))); if ($op==='inspect') { $cap->RiskLevel=0; $cap->ReadOnly=1; } else { $sql=trim((string)($args->sql??'')); if (!$this->factory_toolDBSingleStatement($sql)) { $ret->Error='db_sql_single_statement_required'; return $ret; } $first=$this->factory_toolDBStatementKeyword($sql); if (!in_array($first,array('CREATE','ALTER','DROP','TRUNCATE','COMMENT','REINDEX'),true)) { $ret->Error='db_schema_ddl_required'; return $ret; } $cap->RiskLevel=$this->factory_toolDBRiskForSQL($sql,3); $cap->ReadOnly=0; } } else if ($name==='db_execute') { $sql=trim((string)($args->sql??'')); if (!$this->factory_toolDBSingleStatement($sql)) { $ret->Error='db_sql_single_statement_required'; return $ret; } if ($this->factory_toolDBRiskForSQL($sql,3)===0) { $ret->Error='db_execute_read_use_db_read'; return $ret; } $cap->RiskLevel=$this->factory_toolDBRiskForSQL($sql,3); $args->expected_max_rows=max(0,min(1000000,(int)($args->expected_max_rows??0))); $args->max_bytes=max(1024,min(262144,(int)($args->max_bytes??131072))); } $ret->OK=true; $ret->Arguments=$args; $ret->Capability=$cap; return $ret; } function factory_toolDBBuildWhere($where=array()) { if (!is_array($where) || !cf_sizeof($where)) return ''; $parts=array(); foreach($where as $item) { if (is_array($item)) $item=(object)$item; if (!is_object($item)) continue; $field=$this->factory_toolDBIdentifier($item->field??''); if ($field===false) return false; $op=strtoupper(trim((string)($item->operator??'='))); if (!in_array($op,array('=','!=','<>','>','>=','<','<=','LIKE','ILIKE','IS NULL','IS NOT NULL'),true)) return false; if ($op==='IS NULL' || $op==='IS NOT NULL') { $parts[]=$field.' '.$op; } else { $parts[]=$field.' '.$op.' '.$this->factory_toolDBValue($item->value??null); } } return cf_sizeof($parts)?' WHERE '.implode(' AND ',$parts):''; } function factory_toolDBRowsResult($rows=array(),$MaxBytes=131072) { $ret=(object)array( 'OK'=>true, 'Status'=>'done', 'Count'=>0, 'Rows'=>array(), 'Truncated'=>false ); $max=max(1024,min(262144,(int)$MaxBytes)); foreach((array)$rows as $row) { $clean=$this->factory_toolDBSanitize($row); $test=$ret->Rows; $test[]=$clean; $json=$this->factory_toolJson($test); if (strlen($json)>$max) { $ret->Truncated=true; break; } $ret->Rows[]=$clean; } $ret->Count=cf_sizeof($ret->Rows); return $ret; } function factory_toolDBRead($args=false) { $args=is_object($args)?$args:new stdClass; $mode=strtolower(trim((string)($args->mode??'list'))); $maxBytes=(int)($args->max_bytes??131072); if ($mode==='record') { $row=$this->db->getRecord( (string)$args->table, $args->id, $args->field?:'ID', (array)($args->fields??array()) ); return $this->factory_toolDBRowsResult($row?array($row):array(),$maxBytes); } if ($mode==='filter_one') { $filters=new stdClass; foreach((array)($args->where??array()) as $item) { if (is_array($item)) $item=(object)$item; if (!is_object($item) || strtoupper((string)($item->operator??''))!=='=') continue; $filters->{$item->field}=$item->value; } $row=$this->db->getRecordEx((string)$args->table,$filters,(string)($args->order_by??'')); return $this->factory_toolDBRowsResult($row?array($row):array(),$maxBytes); } if ($mode==='sql') { $sql=trim((string)$args->sql); } else { $table=$this->factory_toolDBIdentifier($args->table??''); if ($table===false) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_table_identifier_invalid'); $fields='*'; if (isset($args->fields) && is_array($args->fields) && cf_sizeof($args->fields)) { $tmp=array(); foreach($args->fields as $field) { $id=$this->factory_toolDBIdentifier($field); if ($id===false) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_field_identifier_invalid'); $tmp[]=$id; } $fields=implode(', ',$tmp); } $where=$this->factory_toolDBBuildWhere($args->where??array()); if ($where===false) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_where_invalid'); $order=''; if (!empty($args->order_by)) { $ob=$this->factory_toolDBIdentifier($args->order_by); if ($ob===false) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_order_identifier_invalid'); $dir=strtolower((string)($args->order_direction??'asc'))==='desc'?'DESC':'ASC'; $order=' ORDER BY '.$ob.' '.$dir; } $limit=max(1,min(200,(int)($args->limit??50))); $sql='SELECT '.$fields.' FROM '.$table.$where.$order.' LIMIT '.$limit.';'; } $this->db->init(); $pdo=$this->db->DBH; if (!$pdo) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'database_unavailable'); $started=false; try { if (!$pdo->inTransaction()) { $pdo->beginTransaction(); $started=true; } $pdo->exec('SET TRANSACTION READ ONLY'); $rows=$this->db->query($sql); if ($started && $pdo->inTransaction()) $pdo->rollBack(); if (is_array($rows) && isset($rows['Error'])) { return (object)array( 'OK'=>false, 'Status'=>'failed', 'Error'=>'db_read_failed', 'DatabaseError'=>(string)$rows['Error'] ); } return $this->factory_toolDBRowsResult($rows,$maxBytes); } catch (Throwable $e) { if ($started && $pdo->inTransaction()) $pdo->rollBack(); return (object)array( 'OK'=>false, 'Status'=>'failed', 'Error'=>'db_read_exception', 'ErrorMessage'=>$e->getMessage() ); } } function factory_toolDBPairsObject($pairs=array()) { $ret=new stdClass; foreach((array)$pairs as $pair) { if (is_array($pair)) $pair=(object)$pair; if (!is_object($pair)) continue; $name=trim((string)($pair->name??'')); if ($this->factory_toolDBIdentifier($name)===false) return false; $ret->$name=$pair->value??null; } return $ret; } function factory_toolDBCountByKey($table,$key,$value) { $t=$this->factory_toolDBIdentifier($table); $k=$this->factory_toolDBIdentifier($key); if ($t===false || $k===false) return false; $r=$this->db->query_first( 'SELECT COUNT(*) AS `Cnt` FROM '.$t.' WHERE '.$k.'='.$this->factory_toolDBValue($value).';' ); return $r?(int)($r->Cnt??0):0; } function factory_toolDBWrite($args=false) { $args=is_object($args)?$args:new stdClass; $op=strtolower(trim((string)($args->operation??''))); $table=(string)($args->table??''); $values=$this->factory_toolDBPairsObject($args->values??array()); if ($values===false) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_values_invalid'); if ($op==='insert') { $r=$this->db->addRecord($table,$values,true); } else if ($op==='update' || $op==='upsert') { $key=trim((string)($args->key_name??'')); $count=$this->factory_toolDBCountByKey($table,$key,$args->key_value); if ($count===false) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_key_invalid'); if ($count>(int)$args->expected_max_rows) { return (object)array('OK'=>false,'Status'=>'row_limit_exceeded','MatchedRows'=>$count); } if ($op==='upsert' && $count===0) { $values->$key=$args->key_value; $r=$this->db->addRecord($table,$values,true); } else { $values->$key=$args->key_value; $r=$this->db->saveRecord($table,$values,$key,false,true); } } else if ($op==='delete') { $key=trim((string)($args->key_name??'')); $count=$this->factory_toolDBCountByKey($table,$key,$args->key_value); if ($count===false) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_key_invalid'); if ($count>(int)$args->expected_max_rows) { return (object)array('OK'=>false,'Status'=>'row_limit_exceeded','MatchedRows'=>$count); } if ($key==='ID') { $r=$this->db->deleteRow($table,$args->key_value); } else { $t=$this->factory_toolDBIdentifier($table); $k=$this->factory_toolDBIdentifier($key); $r=$this->db->exec( 'DELETE FROM '.$t.' WHERE '.$k.'='.$this->factory_toolDBValue($args->key_value).';', 1 ); } } else { return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_write_operation_invalid'); } if (!$r) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_write_failed'); $err=trim((string)($r->Error??'')); if ($err!=='') { return (object)array( 'OK'=>false, 'Status'=>'failed', 'Error'=>'db_write_failed', 'DatabaseError'=>$err ); } return (object)array( 'OK'=>true, 'Status'=>'done', 'RowsAffected'=>(int)($r->rowsAffected??0), 'LastInsertID'=>(int)($r->lastInsertID??$r->LastInsertID??0) ); } function factory_toolDBSchemaInspect($table='',$MaxBytes=131072) { $where="table_schema='public'"; if (trim((string)$table)!=='') { $where.=" AND table_name='".escape((string)$table)."'"; } $sql="SELECT table_name,column_name,data_type,is_nullable,column_default,ordinal_position FROM information_schema.columns WHERE ".$where." ORDER BY table_name,ordinal_position LIMIT 500;"; $rows=$this->db->query($sql); return $this->factory_toolDBRowsResult($rows,$MaxBytes); } function factory_toolDBExecStatement($sql='',$ExpectedMaxRows=0) { $r=$this->db->exec(trim((string)$sql),1); if (!$r) return (object)array('OK'=>false,'Status'=>'failed','Error'=>'db_execute_failed'); $err=trim((string)($r->Error??'')); if ($err!=='') { return (object)array( 'OK'=>false, 'Status'=>'failed', 'Error'=>'db_execute_failed', 'DatabaseError'=>$err ); } $affected=(int)($r->rowsAffected??0); if ((int)$ExpectedMaxRows>0 && $affected>(int)$ExpectedMaxRows) { return (object)array( 'OK'=>false, 'Status'=>'row_limit_exceeded', 'RowsAffected'=>$affected, 'Warning'=>'statement_already_executed' ); } return (object)array( 'OK'=>true, 'Status'=>'done', 'RowsAffected'=>$affected, 'LastInsertID'=>(int)($r->lastInsertID??$r->LastInsertID??0) ); } function factory_toolDBSchema($args=false) { $args=is_object($args)?$args:new stdClass; if (strtolower((string)($args->operation??''))==='inspect') { return $this->factory_toolDBSchemaInspect( (string)($args->table??''), (int)($args->max_bytes??131072) ); } return $this->factory_toolDBExecStatement((string)($args->sql??''),0); } function factory_toolDBExecute($args=false) { $args=is_object($args)?$args:new stdClass; return $this->factory_toolDBExecStatement( (string)($args->sql??''), (int)($args->expected_max_rows??0) ); } function factory_toolActionsDispatchAllowlist($Context=false) { $ctx=$Context && is_object($Context)?$Context:new stdClass; $raw=false; if (property_exists($ctx,'GitHubActionsDispatchAllowlist')) $raw=$ctx->GitHubActionsDispatchAllowlist; $projectID=(int)($ctx->ProjectID??0); if ($raw===false && $projectID>0 && method_exists($this,'factory_getProjectSetting')) { try { $row=$this->factory_getProjectSetting($projectID,'hf3.github_actions_dispatch_allowlist'); if ($row && isset($row->Value)) $raw=$row->Value; } catch (Throwable $e) { $raw=false; } } $decoded=$this->factory_toolDecodeJsonValue($raw); if ($decoded===false) return array(); if (is_object($decoded) && isset($decoded->items) && is_array($decoded->items)) $decoded=$decoded->items; else if (is_object($decoded) && isset($decoded->repository)) $decoded=array($decoded); else if (is_object($decoded)) $decoded=array_values((array)$decoded); if (!is_array($decoded)) return array(); $ret=array(); foreach($decoded as $item) { if (is_array($item)) $item=(object)$item; if (!is_object($item) || isset($item->enabled) && !$item->enabled) continue; $ret[]=$item; } return $ret; } function factory_toolActionsDispatchInputMap($Pairs=false) { $ret=(object)array('OK'=>false,'Inputs'=>array(),'Error'=>''); if (!is_array($Pairs)) { $ret->Error='github_actions_dispatch_inputs_not_array'; return $ret; } if (cf_sizeof($Pairs)>25) { $ret->Error='github_actions_dispatch_inputs_too_many'; return $ret; } $inputs=array(); foreach($Pairs as $pair) { if (is_array($pair)) $pair=(object)$pair; if (!is_object($pair)) { $ret->Error='github_actions_dispatch_input_invalid'; return $ret; } $keys=array_keys((array)$pair); sort($keys,SORT_STRING); if ($keys!==array('name','value')) { $ret->Error='github_actions_dispatch_input_shape'; return $ret; } $name=trim((string)($pair->name??'')); if ($name==='' || !preg_match('/^[A-Za-z0-9_.-]{1,80}$/D',$name)) { $ret->Error='github_actions_dispatch_input_name'; return $ret; } if (array_key_exists($name,$inputs)) { $ret->Error='github_actions_dispatch_input_duplicate:'.$name; return $ret; } if (!is_string($pair->value??null) || strlen($pair->value)>4096) { $ret->Error='github_actions_dispatch_input_value:'.$name; return $ret; } $inputs[$name]=$pair->value; } $ret->OK=true; $ret->Inputs=$inputs; return $ret; } function factory_toolActionsDispatchPreflight($Arguments=false,$Context=false,$Capability=false) { $ret=(object)array('OK'=>false,'Error'=>''); $args=is_object($Arguments)?clone $Arguments:new stdClass; $repository=trim((string)($args->repository??'')); if (method_exists($this,'github_repoNormalize')) $repository=(string)$this->github_repoNormalize($repository); if ($repository==='' || !preg_match('/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/D',$repository)) { $ret->Error='github_actions_dispatch_repository_invalid'; return $ret; } $workflow=trim((string)($args->workflow??'')); if (method_exists($this,'github_actionsWorkflowID')) $workflow=(string)$this->github_actionsWorkflowID($workflow); if ($workflow==='' || !preg_match('/^(?:[0-9]+|[A-Za-z0-9_.-]+)$/D',$workflow)) { $ret->Error='github_actions_dispatch_workflow_invalid'; return $ret; } $ref=trim((string)($args->ref??'')); if ($ref==='' || strlen($ref)>255 || preg_match('/[\x00-\x20~^:?*\\[\\]]/',$ref)) { $ret->Error='github_actions_dispatch_ref_invalid'; return $ret; } $mapped=$this->factory_toolActionsDispatchInputMap($args->inputs??false); if (!$mapped->OK) { $ret->Error=$mapped->Error; return $ret; } $match=false; foreach($this->factory_toolActionsDispatchAllowlist($Context) as $item) { $repo=trim((string)($item->repository??'')); if (method_exists($this,'github_repoNormalize')) $repo=(string)$this->github_repoNormalize($repo); $wf=trim((string)($item->workflow??'')); if (method_exists($this,'github_actionsWorkflowID')) $wf=(string)$this->github_actionsWorkflowID($wf); if ($repo!==$repository || $wf!==$workflow) continue; $refs=$item->refs??array(); if (is_string($refs)) $refs=array($refs); if (!is_array($refs) || !in_array($ref,$refs,true)) continue; $match=$item; break; } if (!$match) { $ret->Error='github_actions_dispatch_not_allowlisted'; return $ret; } $rules=$match->inputs??new stdClass; if (is_array($rules)) $rules=(object)$rules; if (!is_object($rules)) $rules=new stdClass; $required=$match->required_inputs??array(); if (is_string($required)) $required=array($required); if (!is_array($required)) $required=array(); foreach($required as $name) { $name=(string)$name; if (!array_key_exists($name,$mapped->Inputs)) { $ret->Error='github_actions_dispatch_input_required:'.$name; return $ret; } } foreach($mapped->Inputs as $name=>$value) { if (!property_exists($rules,$name)) { $ret->Error='github_actions_dispatch_input_not_allowlisted:'.$name; return $ret; } $allowed=$rules->$name; if (is_string($allowed)) $allowed=array($allowed); if (!is_array($allowed) || !in_array($value,$allowed,true)) { $ret->Error='github_actions_dispatch_input_value_not_allowlisted:'.$name; return $ret; } } $normalized=clone $args; $normalized->repository=$repository; $normalized->workflow=$workflow; $normalized->ref=$ref; $normalized->inputs=(object)$mapped->Inputs; $cap=$Capability && is_object($Capability)?clone $Capability:new stdClass; $allowRisk=max(0,min(3,(int)($match->risk_level??($match->RiskLevel??0)))); $cap->RiskLevel=max((int)($cap->RiskLevel??0),$allowRisk); $ret->OK=true; $ret->Arguments=$normalized; $ret->Capability=$cap; return $ret; } function factory_toolPreflight($Name='',$Arguments=false,$Context=false,$Capability=false) { $name=trim((string)$Name); if ($name==='github_actions_dispatch') return $this->factory_toolActionsDispatchPreflight($Arguments,$Context,$Capability); return (object)array( 'OK'=>true, 'Arguments'=>is_object($Arguments)?$Arguments:new stdClass, 'Capability'=>$Capability ); } function factory_toolCapabilityVisible($Capability=false,$Context=false) { if (!$Capability || !is_object($Capability)) return false; $code=trim((string)($Capability->Code??'')); if ($code==='github_actions_dispatch') return cf_sizeof($this->factory_toolActionsDispatchAllowlist($Context))>0; return true; } function factory_toolCapabilityToOpenAI($Capability=false) { if (!$Capability || !is_object($Capability)) return false; $schema=$this->factory_toolDecodeJsonObject($Capability->ParamsSchema??false); if (!$schema) return false; $tool=new stdClass; $tool->type='function'; $tool->name=(string)$Capability->Code; $tool->description=(string)$Capability->Description; $tool->parameters=$schema; $tool->strict=true; return $tool; } function factory_toolOpenAITools($Codes=false,$Context=false) { $this->factory_toolEnsureCoreCapabilities(); $where="c.`WorkerID`='0' AND COALESCE(c.`Enabled`,1)=1"; if (is_array($Codes) && cf_sizeof($Codes)) { $quoted=array(); foreach($Codes as $code) $quoted[]="'".escape(trim((string)$code))."'"; $where.=" AND c.`Code` IN (".implode(',',$quoted).")"; } $rows=$this->db->query_assoc("SELECT c.* FROM `factory_capabilities` c WHERE ".$where." ORDER BY c.`Code`,c.`ID`;"); $ret=array(); foreach((array)$rows as $row) { if (!$this->factory_toolCapabilityVisible($row,$Context)) continue; $tool=$this->factory_toolCapabilityToOpenAI($row); if ($tool) $ret[]=$tool; } return $ret; } function factory_toolAutonomyLevel($Context=false) { if ($Context && is_object($Context) && isset($Context->AutonomyLevel)) return max(0,min(3,(int)$Context->AutonomyLevel)); $projectID=$Context && is_object($Context) ? (int)($Context->ProjectID??0) : 0; if ($projectID>0 && method_exists($this,'factory_getProjectSetting')) { $row=$this->factory_getProjectSetting($projectID,'hf3.autonomy_level'); if ($row && isset($row->Value) && is_numeric($row->Value)) return max(0,min(3,(int)$row->Value)); } return 2; } function factory_toolRiskDecision($Capability=false,$Context=false) { $ret=(object)array('Allowed'=>false,'RequiresHuman'=>false,'RiskLevel'=>0,'AutonomyLevel'=>$this->factory_toolAutonomyLevel($Context),'Reason'=>''); if (!$Capability || !is_object($Capability)) { $ret->Reason='capability_missing'; return $ret; } // Canonical HF 3.0 scale: 0=low, 1=medium, 2=high, 3=critical. $risk=max(0,min(3,(int)($Capability->RiskLevel??0))); $ret->RiskLevel=$risk; if (!empty($Capability->RequiresHuman)) { $ret->RequiresHuman=true; $ret->Reason='capability_requires_human'; return $ret; } $ctx=$Context && is_object($Context)?$Context:new stdClass; $level=$ret->AutonomyLevel; $reversible=!empty($ctx->Reversible); $recovery=!empty($ctx->RecoveryVerified); $withinContract=!empty($ctx->WithinContract) || !empty($ctx->InScope) || !empty($ctx->ContractAllows); $sandboxed=!empty($ctx->Sandboxed); $criticalAllowed=!empty($ctx->CriticalAllowed); if ($risk===0) { $ret->Allowed=true; $ret->Reason='low_risk'; return $ret; } if ($level===0) { $ret->RequiresHuman=true; $ret->Reason='strict_project_policy'; return $ret; } if ($level===1) { if ($risk===1 && $reversible && $withinContract) { $ret->Allowed=true; $ret->Reason='moderate_medium_reversible_in_scope'; } else { $ret->RequiresHuman=true; $ret->Reason='moderate_project_risk_gate'; } return $ret; } if ($level===2) { if ($risk===1) { $ret->Allowed=true; $ret->Reason='autonomous_medium_risk'; return $ret; } if ($risk===2 && $reversible && $recovery && $withinContract) { $ret->Allowed=true; $ret->Reason='autonomous_high_reversible_verified_in_scope'; return $ret; } $ret->RequiresHuman=true; $ret->Reason=$risk===3?'autonomous_critical_gate':'autonomous_high_risk_gate'; return $ret; } // Level 3 is permissive only inside an explicitly isolated recoverable sandbox. if ($risk===1) { $ret->Allowed=true; $ret->Reason='experimental_medium_risk'; return $ret; } if ($risk===2 && $sandboxed && $recovery) { $ret->Allowed=true; $ret->Reason='experimental_high_sandboxed'; return $ret; } if ($risk===3 && $sandboxed && $recovery && $criticalAllowed) { $ret->Allowed=true; $ret->Reason='experimental_critical_sandbox_authorized'; return $ret; } $ret->RequiresHuman=true; $ret->Reason=$risk===3?'critical_not_authorized':'experimental_high_not_sandboxed'; return $ret; } function factory_toolCorrelationID($Request=false) { $ctx=$Request && isset($Request->Context) && is_object($Request->Context)?$Request->Context:new stdClass; $id=trim((string)($ctx->CorrelationID??'')); if ($id!=='') return $id; if (method_exists($this,'factory_developmentLoopCorrelationID')) return $this->factory_developmentLoopCorrelationID('hf3-tool'); return 'hf3-tool-'.date('YmdHis').'-'.bin2hex(random_bytes(6)); } function factory_toolCreateCommand($Request,$Capability,$Risk,$Status='running') { $ret=(object)array('OK'=>false,'CommandID'=>0,'CorrelationID'=>''); if (!isset($this->db) || !$this->db) { $ret->Error='database_unavailable'; return $ret; } $ctx=is_object($Request->Context??null)?$Request->Context:new stdClass; $correlation=$this->factory_toolCorrelationID($Request); $data=(object)array( 'HF3'=>1, 'ToolArguments'=>$Request->Arguments??new stdClass, 'OpenAICallID'=>(string)($Request->CallID??''), 'OpenAIResponseID'=>(string)($ctx->ResponseID??''), 'ProviderType'=>(string)($Capability->ProviderType??''), 'RequestedByRole'=>(string)($ctx->RoleCode??''), 'AgentStep'=>(int)($ctx->AgentStep??0), 'AutonomyLevel'=>(int)$Risk->AutonomyLevel, 'RiskReason'=>(string)$Risk->Reason ); $rec=new stdClass; if ((int)($ctx->RunID??0)>0) $rec->RunID=(int)$ctx->RunID; if ((int)($ctx->SubtaskID??0)>0) $rec->SubtaskID=(int)$ctx->SubtaskID; if ((int)($ctx->TaskID??0)>0) $rec->TaskID=(int)$ctx->TaskID; if ((int)($ctx->ProjectID??0)>0) $rec->ProjectID=(int)$ctx->ProjectID; if ((int)($ctx->UserID??0)>0) $rec->CreatedBy=(int)$ctx->UserID; $rec->CorrelationID=$correlation; $rec->CommandType=(string)$Capability->Code; $rec->CommandData=$this->factory_toolJson($data); $rec->Status=(string)$Status; $rec->RiskLevel=(int)$Risk->RiskLevel; $rec->RequiresApproval=$Risk->RequiresHuman?1:0; $rec->CreatedAt=date('Y-m-d H:i:s'); if ($Status==='running') $rec->StartedAt=date('Y-m-d H:i:s'); $ins=$this->db->addRecord('factory_commands',$rec); $id=(int)($ins->lastInsertID??0); if ($id<1) { $ret->Error='command_insert_failed'; return $ret; } $ret->OK=true; $ret->CommandID=$id; $ret->CorrelationID=$correlation; return $ret; } function factory_toolFinishCommand($CommandID,$Status,$Result=false,$Error='') { $CommandID=(int)$CommandID; if ($CommandID<1) return false; $rec=new stdClass; $rec->ID=$CommandID; $rec->Status=trim((string)$Status); $rec->FinishedAt=date('Y-m-d H:i:s'); if ($Result!==false) $rec->OutputText=$this->factory_toolJson($Result); if (trim((string)$Error)!=='') $rec->ErrorText=trim((string)$Error); $this->db->saveRecord('factory_commands',$rec); return true; } function factory_toolRunnerRegistry() { $runners=getFromCache('github_runners'); $runners=(array)$runners; $path=getrootdirsrv().'data/runner_status/'; if (is_dir($path)) { $files=glob($path.'*.json'); if (is_array($files)) foreach($files as $fname) { if (!is_file($fname) || !is_readable($fname)) continue; $raw=@file_get_contents($fname); if (!$raw) continue; $item=json_decode($raw); if (!$item || !is_object($item) || (string)($item->schema??'')!=='cf.github.runner.status.v1') continue; $id=trim((string)($item->node->id??'')); if ($id==='') continue; $incoming=(int)($item->generated_at_epoch??0); $cached=$runners[$id]??false; $cachedEpoch=$cached && is_object($cached)?(int)($cached->generated_at_epoch??0):0; if (!$cached || $incoming>=$cachedEpoch) $runners[$id]=$item; } } ksort($runners,SORT_STRING); return $runners; } function factory_toolRunnerSummary($item=false) { if (!$item || !is_object($item)) return false; $epoch=(int)($item->generated_at_epoch??0); $age=$epoch>0?max(0,time()-$epoch):null; return (object)array( 'node'=>(string)($item->node->id??''), 'generated_at'=>(string)($item->generated_at??''), 'generated_at_epoch'=>$epoch, 'age_seconds'=>$age, 'fresh'=>$age!==null && $age<=180, 'state'=>(string)($item->status->state??'unknown'), 'worker_count'=>(int)($item->status->worker_count??0), 'host'=>(string)($item->host->name??''), 'os'=>(string)($item->host->os??''), 'runner_name'=>(string)($item->runner->name??''), 'runner_version'=>(string)($item->runner->version??''), 'service_state'=>(string)($item->service->active_state??($item->service->state??'')), 'runner_can_write_thesystem'=>isset($item->thesystem->runner_can_write_root)?(bool)$item->thesystem->runner_can_write_root:null, 'notification_url'=>(string)($item->notification->url??'') ); } function factory_toolRunnerGetStatus($Arguments=false) { $args=is_object($Arguments)?$Arguments:new stdClass; $node=property_exists($args,'node') && $args->node!==null ? trim((string)$args->node) : ''; $runners=$this->factory_toolRunnerRegistry(); if ($node!=='') { if (!isset($runners[$node])) return (object)array('OK'=>false,'Status'=>'not_found','Node'=>$node); $item=$this->factory_toolRunnerSummary($runners[$node]); return (object)array('OK'=>true,'Status'=>!empty($item->fresh)?'done':'stale','Node'=>$node,'Item'=>$item); } $items=array(); foreach($runners as $id=>$runner) { $summary=$this->factory_toolRunnerSummary($runner); if ($summary) $items[$id]=$summary; } return (object)array('OK'=>true,'Status'=>'done','Count'=>cf_sizeof($items),'Items'=>$items); } function factory_toolCreateDecision($Request,$Capability,$Risk,$CommandID) { if (!method_exists($this,'factory_createDecision')) return false; $ctx=is_object($Request->Context??null)?$Request->Context:new stdClass; $d=new stdClass; if ((int)($ctx->ProjectID??0)>0) $d->ProjectID=(int)$ctx->ProjectID; if ((int)($ctx->TaskID??0)>0) $d->TaskID=(int)$ctx->TaskID; if ((int)($ctx->SubtaskID??0)>0) $d->SubtaskID=(int)$ctx->SubtaskID; if ((int)($ctx->RunID??0)>0) $d->RunID=(int)$ctx->RunID; $d->CommandID=(int)$CommandID; $d->CorrelationID=$this->factory_toolCorrelationID($Request); $d->DecisionType='hf3_tool_approval'; $d->DecisionKey='hf3_tool:'.sha1((string)$Capability->Code.'|'.$this->factory_toolJson($Request->Arguments??new stdClass).'|'.(string)($d->TaskID??0)); $d->Title='Подтвердить действие '.$Capability->Code; $d->Question='Инструмент требует участия человека по политике риска проекта.'; $d->ProposalData=(object)array('Tool'=>$Capability->Code,'Arguments'=>$Request->Arguments??new stdClass,'RiskLevel'=>(int)$Risk->RiskLevel,'AutonomyLevel'=>(int)$Risk->AutonomyLevel,'Reason'=>(string)$Risk->Reason); $d->RiskLevel=min(3,(int)$Risk->RiskLevel); $d->IsBlocking=1; return $this->factory_createDecision($d,(int)($ctx->UserID??0)); } function factory_toolExecute($Request=false) { $ret=(object)array('OK'=>false,'Status'=>'failed'); if (!$Request || !is_object($Request)) { $ret->Error='tool_request_invalid'; return $ret; } $name=trim((string)($Request->Name??'')); if ($name==='') { $ret->Error='tool_name_empty'; return $ret; } $cap=$this->factory_toolCapability($name); if (!$cap) { $ret->Error='tool_not_registered'; $ret->Tool=$name; return $ret; } $schema=$this->factory_toolDecodeJsonObject($cap->ParamsSchema??false); $args=is_object($Request->Arguments??null)?$Request->Arguments:new stdClass; $valid=$this->factory_toolValidateArguments($schema,$args); if (!$valid->OK) { $ret->Error=$valid->Error; $ret->Tool=$name; return $ret; } $ctx=is_object($Request->Context??null)?$Request->Context:new stdClass; $preflight=$this->factory_toolPreflight($name,$args,$ctx,$cap); if (!$preflight->OK) { $ret->Error=(string)($preflight->Error??'tool_preflight_failed'); $ret->Tool=$name; return $ret; } if (isset($preflight->Arguments) && is_object($preflight->Arguments)) $args=$preflight->Arguments; if (isset($preflight->Capability) && is_object($preflight->Capability)) $cap=$preflight->Capability; $effectiveRequest=clone $Request; $effectiveRequest->Arguments=$args; $risk=$this->factory_toolRiskDecision($cap,$ctx); $status=$risk->RequiresHuman?'waiting_human':'running'; $command=$this->factory_toolCreateCommand($effectiveRequest,$cap,$risk,$status); if (!$command->OK) { $ret->Error=$command->Error??'command_create_failed'; return $ret; } $ret->CommandID=(int)$command->CommandID; $ret->CorrelationID=(string)$command->CorrelationID; $ret->Tool=$name; $ret->Provider=(string)($cap->ProviderType??''); $ret->RiskLevel=(int)$risk->RiskLevel; $ret->AutonomyLevel=(int)$risk->AutonomyLevel; if ($risk->RequiresHuman || !$risk->Allowed) { $decision=$this->factory_toolCreateDecision($effectiveRequest,$cap,$risk,$ret->CommandID); $ret->OK=true; $ret->Status='waiting_human'; $ret->DecisionID=(int)($decision->DecisionID??0); $ret->Reason=(string)$risk->Reason; return $ret; } try { if ((string)$cap->ProviderType==='thesystem' && $name==='runner_get_status') { $result=$this->factory_toolRunnerGetStatus($args); } else if ((string)$cap->ProviderType==='github' && $name==='github_read_file') { if (!method_exists($this,'github_repoReadFile')) { $result=(object)array('OK'=>false,'Status'=>'failed','Error'=>'github_provider_unavailable'); } else { $result=$this->github_repoReadFile( (string)($args->repository??''), (string)($args->path??''), property_exists($args,'ref')?$args->ref:false, (int)($args->max_bytes??262144) ); } } else if ((string)$cap->ProviderType==='github' && $name==='github_actions_get_run') { $result=method_exists($this,'github_actionsGetRun') ? $this->github_actionsGetRun((string)($args->repository??''),(int)($args->run_id??0)) : (object)array('OK'=>false,'Status'=>'failed','Error'=>'github_provider_unavailable'); } else if ((string)$cap->ProviderType==='github' && $name==='github_actions_get_jobs') { $result=method_exists($this,'github_actionsGetJobs') ? $this->github_actionsGetJobs((string)($args->repository??''),(int)($args->run_id??0),(string)($args->filter??'latest')) : (object)array('OK'=>false,'Status'=>'failed','Error'=>'github_provider_unavailable'); } else if ((string)$cap->ProviderType==='github' && $name==='github_actions_get_job_logs') { $result=method_exists($this,'github_actionsGetJobLogs') ? $this->github_actionsGetJobLogs((string)($args->repository??''),(int)($args->job_id??0),(int)($args->max_bytes??1048576)) : (object)array('OK'=>false,'Status'=>'failed','Error'=>'github_provider_unavailable'); } else if ((string)$cap->ProviderType==='github' && $name==='github_actions_dispatch') { if (!method_exists($this,'github_actionsDispatch')) { $result=(object)array('OK'=>false,'Status'=>'failed','Error'=>'github_provider_unavailable'); } else { $inputs=is_object($args->inputs??null)?(array)$args->inputs:array(); $result=$this->github_actionsDispatch( (string)($args->repository??''), (string)($args->workflow??''), (string)($args->ref??''), $inputs ); } } else { $result=(object)array('OK'=>false,'Status'=>'failed','Error'=>'tool_provider_not_implemented'); } } catch (Throwable $e) { $result=(object)array('OK'=>false,'Status'=>'failed','Error'=>'tool_provider_exception','ErrorMessage'=>$e->getMessage()); } $ok=is_object($result) && !empty($result->OK); $finalStatus=$ok?'done':'failed'; $error=$ok?'':trim((string)($result->Error??$result->Message??'tool_execution_failed')); $this->factory_toolFinishCommand($ret->CommandID,$finalStatus,$result,$error); $ret->OK=$ok; $ret->Status=$finalStatus; $ret->Result=$result; if (!$ok) $ret->Error=$error; return $ret; } } ?> Redirecting… Redirecting…