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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e272a00a9f7baaee44c4ad7b9827da6c1e5b930a98228e8d58718474d3090214
4
- data.tar.gz: a7ac75b948c326d0829db56e0ab297561d178fa4643a6a4bbd5b4dc11cff150b
3
+ metadata.gz: 30a90de63f88d65265581ced8533203fefc05704a2eee3b604d0c74fc4d64097
4
+ data.tar.gz: f443e5b20bc11142852e4073772db5d4550263de114fc54d735159d3cb47a2cb
5
5
  SHA512:
6
- metadata.gz: 1e4ff77f5dd524e7bc3217b4f8fc1b8045f43d188b751f81dc2964dcf2d15dbfd3ed58efe8598ba40bd875e19a3597de1308e32c066466713b06af8e79fea55a
7
- data.tar.gz: 3f5f8e5f78bc8db0566102f686ce1b66e752f9f81faba9dae09adfa49883aaa67c621b108b9a60cb573d695cae4874567e2ebf1c69b90a3971a6686f0316c288
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
@@ -11,7 +11,7 @@ and [examples guide](examples/README.md).
11
11
 
12
12
  ```ruby
13
13
  # Add to your Gemfile
14
- gem 'prescient', '~> 0.8.0'
14
+ gem 'prescient', '~> 0.8.1'
15
15
  ```
16
16
 
17
17
  ### 2. Replace Existing AI Service
@@ -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 [Regexp] Fenced JSON action matcher
8
- ACTION_PATTERN = /```json\s*(\{.*?\})\s*```/m
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
- match = text.to_s.match(ACTION_PATTERN)
27
- return nil unless match
28
- raise MalformedActionError, "agent action exceeds configured size limit" if match[1].bytesize > @max_bytes
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(match[1])
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"
@@ -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"
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Prescient
4
4
  # Current Prescient gem version.
5
- VERSION = "0.8.0"
5
+ VERSION = "0.8.1"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prescient
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ken C. Demanawa