Module: PWN::SessionTrace

Defined in:
lib/pwn/session_trace.rb

Overview

Recorded, redacted evidence, not a promise of deterministic LLM output.

Constant Summary collapse

EVENTS =
%w[request response tool_call tool_result].freeze

Class Method Summary collapse

Class Method Details

.append(opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/pwn/session_trace.rb', line 16

public_class_method def self.append(opts = {})
  event = opts[:event].to_s
  raise ArgumentError, 'Invalid trace event' unless EVENTS.include?(event)

  path = trace_path(session_id: opts[:session_id])
  FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
  row = PWN::Redaction.redact(value: {
                                version: 1, event: event, data: opts[:data], model: opts[:model], params: opts[:params] || {}
                              })
  row[:seed] = opts[:seed] if opts.key?(:seed) && !opts[:seed].nil?
  File.open(path, File::RDWR | File::CREAT | File::APPEND | File::NOFOLLOW, 0o600) do |file|
    file.flock(File::LOCK_EX)
    file.rewind
    last = nil
    file.each_line { |line| last = JSON.parse(line) }
    now = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
    row[:monotonic_ns] = [now, last ? last.fetch('monotonic_ns') + 1 : 0].max
    row[:sequence] = last ? last.fetch('sequence') + 1 : 1
    file.puts(JSON.generate(row))
    file.flush
  end
  row
end

.authorsObject



128
129
130
# File 'lib/pwn/session_trace.rb', line 128

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <[email protected]>\n"
end

.helpObject



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/pwn/session_trace.rb', line 132

public_class_method def self.help
  puts "USAGE:
    # Persist one redacted event with monotonic ordering.
    #{self}.append(
      session_id: 'required - safe session identifier',
      event: 'required - request, response, tool_call or tool_result',
      data: 'optional - event payload with call IDs',
      model: 'optional - provider model identifier',
      params: 'optional - model invocation parameters',
      seed: 'optional - provider-supported seed'
    )
    # Render recorded evidence without executing tools.
    #{self}.replay(
      session_id: 'required - recorded session identifier',
      io: 'optional - output IO, defaults to stdout'
    )
    # Read recorded evidence with defensive redaction.
    #{self}.read(
      session_id: 'required - recorded session identifier'
    )
    # Rerun shell/Ruby calls in a fresh no-network Bubblewrap namespace.
    #{self}.rerun(
      session_id: 'required - recorded session identifier',
      environment: 'required - explicit bubblewrap opt-in; no host fallback',
      timeout: 'optional - positive deadline seconds, default 30, maximum 300'
    )
    # Print the author information.
    #{self}.authors
  "
end

.read(opts = {}) ⇒ Object



48
49
50
51
52
53
54
# File 'lib/pwn/session_trace.rb', line 48

public_class_method def self.read(opts = {})
  path = trace_path(session_id: opts[:session_id])
  File.open(path, File::RDONLY | File::NOFOLLOW) do |file|
    file.flock(File::LOCK_SH)
    file.each_line.map { |line| PWN::Redaction.redact(value: JSON.parse(line)) }
  end
end

.replay(opts = {}) ⇒ Object

Read-only rendering; never dispatches a recorded call.



41
42
43
44
45
46
# File 'lib/pwn/session_trace.rb', line 41

public_class_method def self.replay(opts = {})
  rows = read(session_id: opts[:session_id])
  io = opts[:io] || $stdout
  rows.each { |row| io.puts(JSON.generate(row)) }
  rows
end

.rerun(opts = {}) ⇒ Object

Explicit opt-in; no host dispatch, home mount, network or inherited env. Ruby is the isolated system Ruby, not the original provider/runtime.



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/pwn/session_trace.rb', line 58

public_class_method def self.rerun(opts = {})
  raise ArgumentError, 'Select an isolated environment: bubblewrap' unless opts[:environment].to_s == 'bubblewrap'

  calls = read(session_id: opts[:session_id]).select { |row| row['event'] == 'tool_call' }.map { |row| row['data'] }
  calls.each do |call|
    raise ArgumentError, 'Unsupported rerun tool' unless call.is_a?(Hash) && %w[shell pwn_eval].include?(call['name'])
    raise ArgumentError, 'Redacted calls require fresh fixture inputs' if JSON.generate(call).include?('[REDACTED:')
    raise ArgumentError, 'Tool arguments required' unless call['arguments'].is_a?(Hash)
  end
  runner = <<~RUBY
    require 'json'
    require 'open3'
    calls = JSON.parse(STDIN.read)
    results = calls.map do |call|
      args = call.fetch('arguments')
      argv = if call['name'] == 'shell'
               ['/bin/sh', '-c', args.fetch('command')]
             else
               ['/usr/bin/ruby', '-e', "result = eval(ARGV.fetch(0)); puts(result) unless result.nil?", args.fetch('code')]
             end
      out, err, status = Open3.capture3(*argv)
      { id: call['id'], name: call['name'], stdout: out, stderr: err, exit_status: status.exitstatus }
    end
    STDOUT.write(JSON.generate(results))
  RUBY
  argv = ['/usr/bin/bwrap', '--unshare-all', '--die-with-parent', '--new-session',
          '--clearenv', '--setenv', 'PATH', '/usr/bin:/bin', '--setenv', 'HOME', '/work',
          '--ro-bind', '/usr', '/usr', '--symlink', 'usr/bin', '/bin',
          '--symlink', 'usr/lib', '/lib', '--symlink', 'usr/lib64', '/lib64',
          '--proc', '/proc', '--dev', '/dev', '--tmpfs', '/tmp',
          '--dir', '/work', '--chdir', '/work', '/usr/bin/ruby', '-e', runner]
  out = +''
  status = nil
  timeout = Float(opts.fetch(:timeout, 30))
  raise ArgumentError, 'Invalid rerun deadline' unless timeout.positive? && timeout <= 300

  Open3.popen3(*argv, unsetenv_others: true, pgroup: true) do |stdin, stdout, stderr, wait|
    readers = [Thread.new { stdout.read }, Thread.new { stderr.read }]
    begin
      Timeout.timeout(timeout) do
        stdin.write(JSON.generate(calls))
        stdin.close
        out = readers.first.value
        readers.last.value
        status = wait.value
      end
    rescue Timeout::Error
      Process.kill('KILL', -wait.pid)
      raise 'Isolated rerun deadline exceeded; no host fallback'
    ensure
      readers.each(&:join)
    end
  end
  raise 'Isolated rerun failed; no host fallback' unless status.success?

  { run_id: SecureRandom.hex(12), environment: 'bubblewrap', results: PWN::Redaction.redact(value: JSON.parse(out)) }
rescue Errno::ENOENT
  raise 'Isolated environment unavailable; no host fallback'
end