20+ CHANGES_REQUESTED dispositions across PHP MCP services, Go pkg/agentic, hermes_runner_mcp Python server, plugin shell scripts. Highlights: - DatabaseSchema.php: identifier quoting - AwardCredits.php: task row locking order - CreditTransaction.php: fail-fast row decoding - OpenApiGenerator.php: YAML parse handling + uri query params - CaptureDispatchResultJob.php: AgentProfile namespace fix - CreditsController.php: missing workspace_id fail-closed - QueryAuditService.php: prose query false positives + unbounded aggregation - McpHealthService.php: proc_close after timeout + env var resolution - CreditLedger.php + FleetOverview.php: workspace agent + dispatch target validation - McpAgentServerCommand.php: quota burn on failed tool calls - McpMetricsService.php: N-day window consistency - hermes_runner_mcp: API key off command line + invalid method+id + run_id encoding - CircuitBreaker.php: extracted CircuitOpenException class with autoload-correct placement - pkg/agentic + brain + flow: SonarCloud sendMessage/fetchLoopRepoRefs/commitWorkspace/Connect annotations - shell scripts: removed [[ usage for portability 43 files modified, 1 new (CircuitOpenException.php). Verification: gofmt -w + php -l + python3 -m py_compile + bash -n all clean. Touched-package go test passes (pkg/lib/flow, pkg/lib). Full go test ./... blocked by pre-existing dappco.re module graph drift, out of scope. Parked for separate work: - Mantis #1062: go.mod local replace removal (cross-repo architectural) - Mantis #1063: Sonar residual line-length / duplication quality-gate cluster Closes findings on https://github.com/dAppCore/agent/pull/6 Co-authored-by: Codex <noreply@openai.com>
122 lines
3.7 KiB
PHP
122 lines
3.7 KiB
PHP
<?php
|
|
|
|
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Core\Mod\Agentic\Data;
|
|
|
|
use Carbon\CarbonImmutable;
|
|
use Carbon\CarbonInterface;
|
|
|
|
final readonly class CreditTransaction
|
|
{
|
|
public function __construct(
|
|
public ?int $id,
|
|
public int $workspaceId,
|
|
public ?int $fleetNodeId,
|
|
public string $taskType,
|
|
public int $amount,
|
|
public int $balanceAfter,
|
|
public ?string $description,
|
|
public CarbonImmutable $createdAt,
|
|
) {}
|
|
|
|
public static function fromModel(object $entry): self
|
|
{
|
|
$createdAt = $entry->created_at ?? null;
|
|
if ($createdAt === null) {
|
|
throw new \InvalidArgumentException('CreditTransaction requires a created_at value.');
|
|
}
|
|
|
|
if ($createdAt instanceof CarbonImmutable) {
|
|
$immutable = $createdAt;
|
|
} elseif ($createdAt instanceof CarbonInterface) {
|
|
$immutable = CarbonImmutable::instance($createdAt);
|
|
} else {
|
|
try {
|
|
$immutable = CarbonImmutable::parse((string) $createdAt);
|
|
} catch (\Throwable) {
|
|
throw new \InvalidArgumentException('CreditTransaction requires a valid created_at value.');
|
|
}
|
|
}
|
|
|
|
return new self(
|
|
id: self::optionalInt($entry->id ?? null, 'id'),
|
|
workspaceId: self::requireInt($entry->workspace_id ?? null, 'workspace_id'),
|
|
fleetNodeId: self::optionalInt($entry->fleet_node_id ?? null, 'fleet_node_id'),
|
|
taskType: self::requireString($entry->task_type ?? null, 'task_type'),
|
|
amount: self::requireInt($entry->amount ?? null, 'amount'),
|
|
balanceAfter: self::requireInt($entry->balance_after ?? null, 'balance_after'),
|
|
description: isset($entry->description) ? (string) $entry->description : null,
|
|
createdAt: $immutable,
|
|
);
|
|
}
|
|
|
|
public function toArray(): array
|
|
{
|
|
return [
|
|
'id' => $this->id,
|
|
'workspace_id' => $this->workspaceId,
|
|
'fleet_node_id' => $this->fleetNodeId,
|
|
'task_type' => $this->taskType,
|
|
'amount' => $this->amount,
|
|
'balance_after' => $this->balanceAfter,
|
|
'description' => $this->description,
|
|
'created_at' => $this->createdAt->toIso8601String(),
|
|
];
|
|
}
|
|
|
|
private static function requireInt(mixed $value, string $field): int
|
|
{
|
|
if ($value === null) {
|
|
throw new \InvalidArgumentException(sprintf(
|
|
'CreditTransaction requires %s.',
|
|
$field,
|
|
));
|
|
}
|
|
|
|
return self::coerceInt($value, $field);
|
|
}
|
|
|
|
private static function optionalInt(mixed $value, string $field): ?int
|
|
{
|
|
if ($value === null) {
|
|
return null;
|
|
}
|
|
|
|
return self::coerceInt($value, $field);
|
|
}
|
|
|
|
private static function coerceInt(mixed $value, string $field): int
|
|
{
|
|
if (is_int($value)) {
|
|
return $value;
|
|
}
|
|
|
|
if (is_float($value) && floor($value) === $value) {
|
|
return (int) $value;
|
|
}
|
|
|
|
if (is_string($value) && preg_match('/^-?\d+$/', $value) === 1) {
|
|
return (int) $value;
|
|
}
|
|
|
|
throw new \InvalidArgumentException(sprintf(
|
|
'CreditTransaction requires integer %s.',
|
|
$field,
|
|
));
|
|
}
|
|
|
|
private static function requireString(mixed $value, string $field): string
|
|
{
|
|
if (! is_string($value) || trim($value) === '') {
|
|
throw new \InvalidArgumentException(sprintf(
|
|
'CreditTransaction requires non-empty %s.',
|
|
$field,
|
|
));
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
}
|