Claude-Agent
view release on metacpan or search on metacpan
lib/Claude/Agent/MCP/SDKRunner.pm view on Meta::CPAN
# Validate version if provided - semver-like format
die "Invalid version format\n"
if defined($version) && length($version) && $version !~ /^[a-zA-Z0-9._-]{1,50}$/;
# Limit tools_json size to prevent memory exhaustion (1MB limit)
die "tools_json too large (max 1MB)\n" if length($tools_json) > 1_000_000;
my ($tools) = $state->{jsonl}->decode($tools_json);
# Validate decoded structure: must be an array of tool definitions
die "Invalid tools_json: expected array\n" unless ref $tools eq 'ARRAY';
for my $tool (@$tools) {
die "Invalid tool definition: expected hash with 'name' key\n"
unless ref $tool eq 'HASH' && defined $tool->{name};
}
# Build tool lookup
my %tool_by_name = map { $_->{name} => $_ } @$tools;
# Validate socket ownership before connecting (defense-in-depth)
lib/Claude/Agent/Query.pm view on Meta::CPAN
# For ref prompts (streaming input), caller will send messages via send_user_message
return;
}
sub _handle_line {
my ($self, $line) = @_;
return unless defined $line && length $line;
# Use JSON::Lines decode method for single line
my @decoded = $self->_jsonl->decode($line);
if ($log->is_trace) {
$log->trace(sprintf("Raw line length: %d", length($line)));
$log->trace(sprintf("Raw line: %s", $line));
$log->trace(sprintf("Decoded %d objects", scalar(@decoded)));
$log->trace(sprintf("JSON::Lines buffer remaining: %d chars", length($self->_jsonl->remaining)));
if ($self->_jsonl->remaining) {
$log->trace(sprintf("Buffer content (first 200): %s", substr($self->_jsonl->remaining, 0, 200)));
}
}
# Guard against buffer overflow from accumulated malformed data
# Configurable threshold via environment variable, default 1MB, max 10MB
# Note: Typical JSON messages from Claude CLI are 1-50KB; tool results (especially
# large file reads or grep outputs) may be 100KB-1MB. The minimum of 100KB ensures
lib/Claude/Agent/Query.pm view on Meta::CPAN
$self->_jsonl(JSON::Lines->new(
utf8 => 1,
error_cb => sub {
my ($action, $error, $data) = @_;
$log->trace(sprintf("JSON::Lines %s error: %s", $action, $error));
return;
},
));
}
return unless @decoded;
for my $data (@decoded) {
if ($log->is_trace) {
$log->trace(sprintf("Decoded item ref type: %s", ref($data) // "not a ref"));
if (ref $data eq 'HASH') {
$log->trace(sprintf("Hash keys: %s", join(", ", keys %$data)));
}
}
next unless defined $data && ref $data eq 'HASH';
$log->trace(sprintf("Message type in data: %s", $data->{type} // "undef"))
if $log->is_trace;
next unless exists $data->{type}; # Skip malformed/partial JSON data
t/12-sdk-validation.t view on Meta::CPAN
# =============================================================================
# JSON::Lines safety tests
# =============================================================================
subtest 'JSON structure validation' => sub {
use JSON::Lines;
my $jsonl = JSON::Lines->new;
# Valid JSON line
my @decoded = $jsonl->decode('{"type":"test"}');
is(scalar @decoded, 1, 'single object decoded');
is($decoded[0]->{type}, 'test', 'type key extracted');
# Array in JSON Lines
@decoded = $jsonl->decode('[1,2,3]');
is(scalar @decoded, 1, 'array decoded as single item');
is_deeply($decoded[0], [1,2,3], 'array content correct');
# Multiple lines (though JSON::Lines takes one at a time)
my $line1 = '{"a":1}';
my $line2 = '{"b":2}';
@decoded = $jsonl->decode($line1);
is($decoded[0]->{a}, 1, 'first line decoded');
@decoded = $jsonl->decode($line2);
is($decoded[0]->{b}, 2, 'second line decoded');
};
# =============================================================================
# PERL5LIB filtering tests (from SDKServer)
# =============================================================================
subtest 'PERL5LIB path filtering' => sub {
use Cwd qw(abs_path);
use File::Spec;
( run in 0.619 second using v1.01-cache-2.11-cpan-9581c071862 )