prescient 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/INTEGRATION_GUIDE.md +1 -1
- data/lib/prescient/agent/parser.rb +88 -6
- data/lib/prescient/agent.rb +1 -1
- data/lib/prescient/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 30a90de63f88d65265581ced8533203fefc05704a2eee3b604d0c74fc4d64097
|
|
4
|
+
data.tar.gz: f443e5b20bc11142852e4073772db5d4550263de114fc54d735159d3cb47a2cb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ba7c44dd34039b89c57bd1ca8c4aae70a8800681460be5c676e66b02936c4bb77934e49be358e0903f109c8521014f467c906f69df9043bfa1762fb5612b08c
|
|
7
|
+
data.tar.gz: e021453fb08fd1908380248fae250b2b06ee42a5c67e6cb9cb1976fa78a35768dbfd8b80abd2baaaafd3e12e7e4354378ae3ce6fd4020dcde77a99204b9cb394
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## [0.8.1] - 2025-09-10
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Fixed the Agent audit-log load path to avoid a circular-require warning while
|
|
10
|
+
preserving lazy loading of the optional Agent implementation.
|
|
11
|
+
- Replaced the Agent parser's polynomial-time fenced-JSON regular expression
|
|
12
|
+
with a linear scanner to prevent ReDoS behavior on untrusted provider output.
|
|
13
|
+
- Added Agent parser regression coverage for nested objects, escaped string
|
|
14
|
+
values, malformed fences, and repeated unterminated action prefixes.
|
|
15
|
+
|
|
5
16
|
## [0.8.0] - 2025-08-31
|
|
6
17
|
|
|
7
18
|
### Added
|
data/INTEGRATION_GUIDE.md
CHANGED
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
module Prescient::Agent
|
|
5
5
|
# Extracts and validates one tool action from provider text.
|
|
6
6
|
class Parser
|
|
7
|
-
# @return [
|
|
8
|
-
|
|
7
|
+
# @return [String] Opening fenced JSON marker
|
|
8
|
+
JSON_FENCE = "```json".b.freeze
|
|
9
|
+
# @return [String] Closing fenced JSON marker
|
|
10
|
+
CLOSING_FENCE = "```".b.freeze
|
|
11
|
+
# @return [Array<Integer>] JSON whitespace byte values
|
|
12
|
+
WHITESPACE_BYTES = [9, 10, 11, 12, 13, 32].freeze
|
|
9
13
|
|
|
10
14
|
# Parse one optional action from provider output.
|
|
11
15
|
# @param text [String] Provider response text
|
|
@@ -23,11 +27,11 @@ module Prescient::Agent
|
|
|
23
27
|
# @param text [String] Provider response text
|
|
24
28
|
# @return [Hash, nil] Parsed action or nil for a final response
|
|
25
29
|
def parse(text)
|
|
26
|
-
|
|
27
|
-
return nil unless
|
|
28
|
-
raise MalformedActionError, "agent action exceeds configured size limit" if
|
|
30
|
+
action_json = extract_action_json(text.to_s)
|
|
31
|
+
return nil unless action_json
|
|
32
|
+
raise MalformedActionError, "agent action exceeds configured size limit" if action_json.bytesize > @max_bytes
|
|
29
33
|
|
|
30
|
-
payload = JSON.parse(
|
|
34
|
+
payload = JSON.parse(action_json)
|
|
31
35
|
validate_payload(payload)
|
|
32
36
|
{ name: payload.fetch("action").to_sym, arguments: payload.fetch("args") }
|
|
33
37
|
rescue JSON::ParserError => e
|
|
@@ -36,6 +40,84 @@ module Prescient::Agent
|
|
|
36
40
|
|
|
37
41
|
private
|
|
38
42
|
|
|
43
|
+
def extract_action_json(text)
|
|
44
|
+
source = text.b
|
|
45
|
+
cursor = 0
|
|
46
|
+
|
|
47
|
+
while (fence_start = source.index(JSON_FENCE, cursor))
|
|
48
|
+
content_start = skip_whitespace(source, fence_start + JSON_FENCE.bytesize)
|
|
49
|
+
unless source.getbyte(content_start) == 123
|
|
50
|
+
cursor = content_start
|
|
51
|
+
next
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
action_json, next_cursor = scan_json_object(source, content_start)
|
|
55
|
+
return action_json if action_json
|
|
56
|
+
|
|
57
|
+
cursor = next_cursor
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def scan_json_object(source, start)
|
|
64
|
+
depth = 0
|
|
65
|
+
escaped = false
|
|
66
|
+
in_string = false
|
|
67
|
+
index = start
|
|
68
|
+
|
|
69
|
+
while index < source.bytesize
|
|
70
|
+
byte = source.getbyte(index)
|
|
71
|
+
if in_string
|
|
72
|
+
in_string, escaped = string_state(byte, escaped)
|
|
73
|
+
elsif fence_at?(source, index)
|
|
74
|
+
return [nil, index]
|
|
75
|
+
else
|
|
76
|
+
in_string = byte == 34
|
|
77
|
+
depth = depth_after(byte, depth)
|
|
78
|
+
return completed_action(source, start, index) if byte == 125 && depth.zero?
|
|
79
|
+
end
|
|
80
|
+
index += 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
[nil, source.bytesize]
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def string_state(byte, escaped)
|
|
87
|
+
return [true, false] if escaped
|
|
88
|
+
return [true, true] if byte == 92
|
|
89
|
+
return [false, false] if byte == 34
|
|
90
|
+
|
|
91
|
+
[true, false]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def fence_at?(source, index)
|
|
95
|
+
byte = source.getbyte(index)
|
|
96
|
+
byte == 96 && source.byteslice(index, JSON_FENCE.bytesize) == JSON_FENCE
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def depth_after(byte, depth)
|
|
100
|
+
case byte
|
|
101
|
+
when 123 then depth + 1
|
|
102
|
+
when 125 then depth - 1
|
|
103
|
+
else depth
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def completed_action(source, start, index)
|
|
108
|
+
closing_start = skip_whitespace(source, index + 1)
|
|
109
|
+
return [nil, closing_start] unless source.byteslice(closing_start, CLOSING_FENCE.bytesize) == CLOSING_FENCE
|
|
110
|
+
|
|
111
|
+
length = index + 1 - start
|
|
112
|
+
[source.byteslice(start, length).force_encoding(Encoding::UTF_8), index + 1]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def skip_whitespace(source, start)
|
|
116
|
+
index = start
|
|
117
|
+
index += 1 while index < source.bytesize && WHITESPACE_BYTES.include?(source.getbyte(index))
|
|
118
|
+
index
|
|
119
|
+
end
|
|
120
|
+
|
|
39
121
|
def validate_payload(payload)
|
|
40
122
|
unless payload.is_a?(Hash) && payload.keys.sort == %w[action args]
|
|
41
123
|
raise MalformedActionError, "agent action must contain only action and args"
|
data/lib/prescient/agent.rb
CHANGED
|
@@ -5,11 +5,11 @@ require "json"
|
|
|
5
5
|
module Prescient
|
|
6
6
|
# Optional bounded agent orchestration over Prescient Core.
|
|
7
7
|
module Agent
|
|
8
|
+
autoload :AuditLog, "prescient/agent/audit_log"
|
|
8
9
|
end
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
require_relative "agent/configuration"
|
|
12
|
-
require_relative "agent/audit_log"
|
|
13
13
|
require_relative "agent/errors"
|
|
14
14
|
require_relative "agent/error_serializer"
|
|
15
15
|
require_relative "agent/schema_validator"
|
data/lib/prescient/version.rb
CHANGED