pwn 0.5.723 → 0.5.724

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: 5ba71758dbd8a3d5231af6489af4c3dd2bd25c0c16df3fd3292b14a77abdf851
4
- data.tar.gz: 31c99dda2f5bac281c8ba2b4cc7b7b4493f6c3f09cbbe6788a1067d177ec5ecc
3
+ metadata.gz: b1c409d6b05e3c061837ef187c74d22ff5e24d361aee4531be0943714a8f713b
4
+ data.tar.gz: dacfce51246aee0f02c84b5a8eae1651fe2853db2e0f192343c5fc8cfbc5462b
5
5
  SHA512:
6
- metadata.gz: 5f17d3c8729c22425cfef5b71746c82bc03eaec569b2d470a83e0734a72e8f411bf8542bc302091211f5f6dc578e5fb64b5a72f97fdb2d8d2f4b8f9f7601100d
7
- data.tar.gz: 6359d1601fa3cc2f340c3edd8efbe71578c0d580c1c7da6dcc212cd2ab56011455cb10578e11d7be9c359c406b2da5d33a6c1eedaa46f73a5a2a0e59d12c8199
6
+ metadata.gz: 6c29fd2cde7775f1923249f166af195dcc99ed0614c86536d3aeb56bcc7a3d29a28a3483b1b2c25bb736b2b78b28ab92a0906d690cd71d002ff335c41e1e5d78
7
+ data.tar.gz: cbcf28e1681df68a21a1da6f247bf645de861ee2a752af2f3735f09f2a59501bdd9d5fb5bf238dcc39ae2734ea173a3d515ddb3df1de2d7548de4fbbb67c5ea7
@@ -18,8 +18,9 @@ bundle exec rubocop scripts/benchmark_policy.rb
18
18
  The experiment uses Ruby and its standard libraries; it does not require the
19
19
  full application boot sequence. Use a fresh Ruby process, not the live agent
20
20
  console. JSON is printed to stdout and optionally written to `--output`.
21
- `--self-check` runs independent scorer tests and both complete experimental arms;
22
- it prints a short pass message instead of a report. Use the options separately.
21
+ `--self-check` runs independent scorer tests, both complete experimental arms,
22
+ and two fresh snapshot workers; it prints short pass messages instead of a
23
+ report. Use the options separately.
23
24
  The benchmark exits nonzero if persistence changes during evaluation, splits
24
25
  overlap, a negative control succeeds, or a positive control fails. Self-checks
25
26
  also require actual training updates in the on arm and none in the off arm.
@@ -36,8 +37,9 @@ not `Reward.judge`, model prose, or a previously assigned reward. It calls
36
37
  training. This is not a test of Reward's verification-record binding, the live
37
38
  Loop, Metrics learning, memory retrieval, or adapter training.
38
39
 
39
- The new `scripts/` directory keeps this experimental driver outside the installed
40
- CLI and production module tree; no module manifest or generated skill is needed.
40
+ The experimental driver remains in `scripts/`. The opt-in `PolicyEvaluation`
41
+ module launches only this fixed runner; it does not load the live agent in its
42
+ workers. Its module autoload does not enable evaluation or promotion.
41
43
 
42
44
  ## Protocol
43
45
 
@@ -60,6 +62,10 @@ CLI and production module tree; no module manifest or generated skill is needed.
60
62
  answer keys to choose actions. Each call writes into a fresh task directory.
61
63
  Exact artifact correctness supplies a binary training label to Policy; the
62
64
  off arm executes the same work but Policy declines to update.
65
+ Each call has an explicit action ID. Only after the independent artifact
66
+ check, `finish` receives a `controlled_comparison` attribution receipt naming
67
+ that ID. This is a one-action isolated experiment, not a claim that the last
68
+ action in an arbitrary live trace caused the outcome.
63
69
  4. **Held-out evaluation.** Only after training, materialize eight distinct
64
70
  held-out inputs with literal answer keys: four numeric and four inventory
65
71
  tasks. Assert no training input appears in evaluation. Task families and
@@ -117,6 +123,9 @@ No fixed gains are embedded in the report.
117
123
 
118
124
  - **Completion:** tasks with at least one objectively successful attempt divided
119
125
  by evaluated tasks. A failed attempt does not count as task completion.
126
+ - **Artifact check score:** each attempt earns 1.0 only for an exact correct
127
+ artifact with unchanged input, otherwise 0.0; phase score is its mean across
128
+ attempts. Report rows retain the actual artifact body as well as its hash.
120
129
  - **False-success count/rate:** attempts claiming `ok: true` without achieving the
121
130
  task; rate denominator is executed attempts, not tasks. This is false reporting
122
131
  by the handler, not acceptance of that report by the independent checker.
@@ -149,3 +158,139 @@ would undermine the no-network/no-credentials guarantee. A future live-model
149
158
  study should use a separately reviewed runner, identical model/tool budgets,
150
159
  external objective verifiers, frozen held-out evaluation, and actual provider
151
160
  usage records. Do not present this controller experiment as that study.
161
+
162
+ ## Opt-in independent snapshot evaluation (R5)
163
+
164
+ The original commands and `off`/`on` report shape still work. To additionally
165
+ export snapshots and run repeated evaluations in **fresh subprocesses**, use:
166
+
167
+ ```sh
168
+ ruby scripts/benchmark_policy.rb --heldout \
169
+ --snapshot-dir /tmp/pwn-policy-snapshots \
170
+ --output /tmp/pwn-policy-heldout.json
171
+ ```
172
+
173
+ The snapshot directory must be **new**, below `/tmp`, with no symlink parents.
174
+ `off.json` and `on.json` are genuine Policy JSON tables after the same real
175
+ training schedule. Exported `updated_at` metadata is normalized to `null` for
176
+ reproducible snapshot digests; no Q entries or rewards are fabricated. Export
177
+ happens before that arm's evaluation. Neither `--heldout` nor `--snapshot-dir`
178
+ promotes anything or discovers/reads a real home-directory policy.
179
+
180
+ The added `heldout` array contains protocol `pwn-policy-heldout-v2`, for suite
181
+ indices 0 and 1. Each worker runs three frozen arms: `off` (baseline snapshot,
182
+ policy disabled), `baseline` (baseline enabled), and `candidate` (candidate
183
+ enabled). Workers never call begin/observe/finish, warmup, or reset the caller's
184
+ policy. Resets and snapshot installation occur only inside temporary HOME.
185
+ Only fixed local handlers are registered; no provider, shell tool, credential,
186
+ user config, or network client is loaded. Environment variables, including Ruby
187
+ startup hooks, are removed before spawning Ruby. Workers have a 30-second
188
+ deadline; stalled children are killed and reaped. Snapshot inputs must be regular
189
+ non-symlink files, at most 4 MiB, with valid numeric `q`, `h`, `visits`, `returns`,
190
+ `n_updates`, and `td_abs_sum` fields. Parent directories cannot be symlinks.
191
+
192
+ Suite indices 0..7 are bounded deterministic variations, **not random trials**.
193
+ Numeric inputs and separate literal answer keys are scaled by `seed + 1`;
194
+ inventory IDs and separate answer keys are offset by `100 * seed`. This does not
195
+ call a candidate algorithm to construct its answer key. Even indices use flat
196
+ paths and compact JSON; odd indices use nested paths containing spaces, pretty
197
+ JSON, and read-only inputs. Every arm runs eight tasks, 16 negative controls and
198
+ eight positive controls. Training fixtures remain unchanged and disjoint.
199
+ These are two task families and two filesystem configurations, not unseen tools
200
+ or broad environment generalization. Both environment types are required for
201
+ promotion eligibility.
202
+ For externally supplied snapshots, `disjoint_inputs` describes the harness's
203
+ fixture sets, not proof of the snapshot's training history; that history is not
204
+ attested by this runner.
205
+
206
+ Explicit snapshots from another controlled experiment can be evaluated without
207
+ booting the application:
208
+
209
+ ```ruby
210
+ require './lib/pwn/ai/agent/policy_evaluation'
211
+ evaluator = PWN::AI::Agent::PolicyEvaluation
212
+ reports = [0, 1].map do |seed|
213
+ evaluator.evaluate(baseline: '/tmp/baseline.json',
214
+ candidate: '/tmp/candidate.json', seed: seed)
215
+ end
216
+ ```
217
+
218
+ With fixed source revisions and snapshot bytes, snapshot reports reproduce all
219
+ fields except `elapsed_seconds`. They contain no wall-clock timestamps, random
220
+ IDs or temporary paths. The original training report still contains the timing
221
+ and metadata variability described above.
222
+
223
+ ## Explicit promotion and rollback
224
+
225
+ This is an **operator-invoked local eligibility gate**, not automatic online
226
+ policy promotion. `Policy.finish` and Loop do not call it. The existing online
227
+ learning behavior is not redirected or promoted by this module. No live policy
228
+ path is defaulted, and writes require `enabled: true` **and** `quiescent: true`.
229
+ The latter is an operator assertion: **stop all agent processes and policy
230
+ writers first**. The existing Policy writer does not share a transaction lock
231
+ with this module; concurrent live learning or concurrent promotions are not
232
+ supported. Restart writers only after the operation and readback complete.
233
+
234
+ Promotion requires 2..8 reports with distinct valid suite indices covering both
235
+ filesystem configurations. It then **reruns each suite in a fresh worker** using
236
+ the specified snapshot bytes. Every non-timing report field must match the fresh
237
+ execution, including source/harness digests, snapshot digests, input hashes,
238
+ artifact bodies and hashes, action choices, scores, controls, and frozen-policy
239
+ checks. Hashes alone are not signatures or evidence of trusted authorship;
240
+ re-execution is the authority. Model-written `passed: true`, edited scores,
241
+ invented artifact bodies, stale source revisions, or copied duplicate reports
242
+ cannot substitute for those executions. Supplied elapsed times are discarded;
243
+ only newly measured times enter the gate.
244
+
245
+ For **every** repeated suite, compared with both baseline-on and off:
246
+
247
+ - completion and mean artifact-check score must not decrease;
248
+ - no previously solved individual task may become unsolved (aggregate gains
249
+ cannot hide a task/family regression);
250
+ - false-success count **and rate**, repeated mistakes and tool calls must not rise;
251
+ - elapsed time must be at most `baseline_seconds * 1.25 + 0.02`, a fixed local
252
+ jitter allowance rather than evidence of a statistically established speedup.
253
+
254
+ Each suite must also improve completion, false-success count, repeated mistakes,
255
+ or calls relative to baseline-on. Better training returns, more updates, or a
256
+ timing-only change cannot qualify. The gate fails closed when verification fails.
257
+ Snapshots, source digests and live-baseline bytes must still match. An explicit
258
+ live target must already exist and equal the evaluated baseline byte-for-byte.
259
+ The previous policy is saved beside it as a digest-named rollback JSON before a
260
+ same-directory atomic replacement and exact readback. No trajectory file is
261
+ modified. Preserve the returned receipt for rollback.
262
+
263
+ A disposable demonstration using the opt-in benchmark output above:
264
+
265
+ ```ruby
266
+ require './lib/pwn/ai/agent/policy_evaluation'
267
+ evaluator = PWN::AI::Agent::PolicyEvaluation
268
+ baseline = '/tmp/pwn-policy-snapshots/off.json'
269
+ candidate = '/tmp/pwn-policy-snapshots/on.json'
270
+ reports = JSON.parse(File.read('/tmp/pwn-policy-heldout.json'), symbolize_names: true).fetch(:heldout)
271
+ live = '/tmp/pwn-policy-demo-live.json' # NOT the real online policy
272
+ File.open(live, File::WRONLY | File::CREAT | File::EXCL, 0o600) { |f| f.write(File.binread(baseline)) }
273
+ receipt = evaluator.promote(enabled: true, quiescent: true,
274
+ baseline: baseline, candidate: candidate,
275
+ reports: reports, live_path: live)
276
+ raise receipt.inspect unless receipt[:promoted]
277
+ restored = evaluator.rollback(enabled: true, quiescent: true,
278
+ live_path: live, receipt: receipt)
279
+ raise restored.inspect unless restored[:rolled_back]
280
+ ```
281
+
282
+ Rollback verifies the receipt's explicit target, backup path and prior digest,
283
+ validates the backup schema, and refuses if the live file no longer matches the
284
+ promoted candidate digest. Missing, altered or symlink backups/targets fail
285
+ closed. Both methods default to a disabled result; rejected operations return
286
+ `promoted: false` or `rolled_back: false` with a reason. `evaluate` raises on an
287
+ invalid snapshot, worker failure, or deadline. Force-killing a worker can leave
288
+ its temporary directory; this is not an OS sandbox for untrusted code.
289
+
290
+ **Limit:** this gate measures only the fixed benchmark action vocabulary and
291
+ public task families. A real online policy containing unrelated tools may show
292
+ no gain and be rejected; passing does not validate those unrelated routes or
293
+ establish live LLM gains. A production rollout still needs separately reviewed,
294
+ representative objective tasks and operator judgment. Do not interpret this
295
+ small public held-out set as a secret test or optimize repeatedly against it
296
+ and then claim independent generalization.
@@ -47,10 +47,10 @@ This is the live numeric controller. It does not replace planning.
47
47
 
48
48
  | Piece | What it is |
49
49
  |---|---|
50
- | State | request kind, task family, plan quality, answer completeness, usable-result, last action, fail bin, and engine |
50
+ | State | request kind, task family, plan quality, answer completeness, usable-result, last action, fail bin, engine, and sanitized host-observed capability/verification scope |
51
51
  | Action | tool name, or `final` |
52
52
  | Step reward | 0; −0.01 per tool after 8 |
53
- | Terminal reward | `Reward.judge` × confidence (sole large R). `plan_coverage` is a tag, not the score. |
53
+ | Terminal reward | Resolved training score mapped to −1..1 × confidence (sole large R), attributed only to independently linked actions. `plan_coverage` is a tag, not the score. |
54
54
  | Updates | Q-learning (`alpha=0.15`, `gamma=0.85`) and REINFORCE (`alpha=0.05`). Stored trajectories replay twice on warmup so a short table is not empty advice. |
55
55
  | Budget | Eight finished episodes (live or warmup-credited) unlock greedy suggestions. Until then the prompt omits them. |
56
56
  | Steer | Q-advantage in `Registry.rank` once the episode budget is met; keyword fit and CORE_TOOLS still come first. Suggested actions follow `Registry.preference_order` (`ai.agent.tool_preference`). |
@@ -81,6 +81,58 @@ current session request and invalidated by subsequent tool execution. The API
81
81
  trusts the host verifier to cover the complete request; it cannot infer missing
82
82
  criteria or automatically verify arbitrary tasks. LLM judgments remain fallible.
83
83
 
84
+ ### Executed acceptance checks and artifact attribution
85
+
86
+ For stronger verification, pass a host-owned `verification_contract` to
87
+ `Loop.run`, or use `Reward.run_verification(request:, session_id:, contract:)`.
88
+ The contract contains `root`, `requirements` (distinct verbatim clauses of the
89
+ original request), and `checks`. Each check names its `requirement` and kind:
90
+
91
+ - `file`: an in-root regular `path` whose bytes equal `expected`.
92
+ - `json`: an in-root regular `path` whose parsed JSON equals `expected`.
93
+ - `command`: a literal `argv`, expected `exit_code` (default zero), and optional
94
+ expected stdout. Requires `allow_commands: true`.
95
+ - `http`: an exact `url` listed in `allowed_urls`, expected response bytes, and
96
+ expected `status` (default 200). GET only, with no redirects.
97
+
98
+ Check failures are negative evidence. Missing requirements, unavailable checks,
99
+ and timeouts stay unknown even if the LLM judge awards a high score. Later tool
100
+ execution invalidates a runner report without falling back to presumed success.
101
+ The runner records output digests, not raw command output or expected values.
102
+ Artifact reads use no-follow descriptors, validate their actual location through
103
+ Linux `/proc/self/fd`, and enforce the byte limit while reading. If descriptor
104
+ location validation is unavailable, the check stays unknown rather than using
105
+ an unsafe pathname fallback.
106
+ The caller must supply the complete acceptance checklist: matching clauses to
107
+ the original text is not semantic proof that the checklist covers every intent.
108
+
109
+ Commands have a cleared environment, private HOME set to the selected root,
110
+ bounded output and timeout, and process-group cleanup. This is **not an OS
111
+ sandbox**: opt-in commands retain the process's filesystem/network permissions.
112
+ Only run trusted checks, using an external sandbox for untrusted programs.
113
+
114
+ At the final boundary, Loop executes the contract and publishes a verification
115
+ event through `on_tool`. Around dispatched actions it snapshots declared
116
+ artifact digests. Matching the final checked digest to its last observed writer
117
+ produces an `independent_verifier` attribution receipt. This establishes artifact
118
+ provenance, not universal causal proof. Controlled comparisons in isolated
119
+ evaluation can supply a separate `controlled_comparison` receipt.
120
+
121
+ Policy distributes the existing terminal reward across uniquely linked action
122
+ IDs. Unlinked actions receive no positive training credit; an accounting-only
123
+ final row retains unattributed terminal reward. Adding successful no-op commands
124
+ does not create more reward. Known per-step cost remains; unknown task outcomes
125
+ still do not train. Ordinary model-judged runs without attribution may retain
126
+ outcome/lesson records, but do not positively reinforce guessed tool contributions.
127
+
128
+ `Loop.run(trusted_context:)` accepts host observations, not model arguments.
129
+ Policy stores only fixed environment/capability/failure/verification categories;
130
+ state backoff stays within that observed scope. Registry checks declared tool
131
+ prerequisites before ranking, including core tools, so a known absent prerequisite
132
+ cannot be outweighed by a historical success score. Unknown availability is not
133
+ treated as absence. Default local scope observes the Ruby runtime and `/bin/sh`;
134
+ callers must supply observations for remote/container-specific capabilities.
135
+
84
136
  Policy observations also capture allowlisted operation, argument-role/type
85
137
  features, and result classification, without retaining raw argument values.
86
138
  These condition next-tool ranking only after enough contextual samples exist,
@@ -12,7 +12,7 @@ metadata:
12
12
 
13
13
  # PWN::AI::Agent::Policy
14
14
 
15
- PWN::AI::Agent::Policy is the LIVE tabular RL controller that pwn-ai did not have before R5. Everything else in the harness is retrieval-plus-policy: scores are written to disk and re-injected as prose, or exported later for optional LoRA. This module is the missing MDP: state s — discretized (kind, task, plan, completeness, usable, last, fail) action a — tool name, or "final" reward r — step: 0 (spam cost −0.01 after 8 tools); terminal: judge × confidence next s' — state after the tool result Each Loop turn is one episode. Transitions land in ~/.pwn/policy_traj.jsonl. Q(s,a) and REINFORCE logits H(s,a) are updated from those tuples and persisted in ~/.pwn/policy.json. The learned Q values are an ADVISORY term in Registry.rank. They never replace TaskSummarizer planning, plan_first, or CORE_TOOLS. Disable with PWN::Env[:ai][:agent][:policy] = false.
15
+ PWN::AI::Agent::Policy is the LIVE tabular RL controller that pwn-ai did not have before R5. Everything else in the harness is retrieval-plus-policy: scores are written to disk and re-injected as prose, or exported later for optional LoRA. This module is the missing MDP: state s — discretized (kind, task, plan, completeness, usable, last, fail) action a — tool name, or "final" reward r — step: 0 (spam cost −0.01 after 8 tools); terminal: judge × confidence next s' — state after the tool result Each Loop turn is one episode. Trusted environment/prerequisite bins scope fallback history; independently evidenced terminal attribution uses isolated action targets, not future-return credit for busywork. Transitions land in ~/.pwn/policy_traj.jsonl. Q(s,a) and REINFORCE logits H(s,a) are updated from those tuples and persisted in ~/.pwn/policy.json. The learned Q values are an ADVISORY term in Registry.rank. They never replace TaskSummarizer planning, plan_first, or CORE_TOOLS. Disable with PWN::Env[:ai][:agent][:policy] = false.
16
16
 
17
17
  ## When to use
18
18
 
@@ -34,6 +34,8 @@ PWN::AI::Agent::Policy.state(opts)
34
34
  ## Public methods
35
35
 
36
36
  - `state`
37
+ - `observed_state`
38
+ - `observed_context`
37
39
  - `cold`
38
40
  - `warm`
39
41
  - `episode_budget_met`
@@ -0,0 +1,49 @@
1
+ ---
2
+ name: pwn-ai-agent-policyevaluation
3
+ description: Drive PWN::AI::Agent::PolicyEvaluation from pwn_eval.
4
+ license: MIT
5
+ allowed-tools: [pwn, pwn_eval]
6
+ metadata:
7
+ bundled: true
8
+ generated: true
9
+ module: PWN::AI::Agent::PolicyEvaluation
10
+ source: pwn/ai/agent/policy_evaluation.rb
11
+ ---
12
+
13
+ # PWN::AI::Agent::PolicyEvaluation
14
+
15
+ Opt-in, fixed local held-out evaluation. Never loaded by the online loop.
16
+
17
+ ## When to use
18
+
19
+ Call `PWN::AI::Agent::PolicyEvaluation` from `pwn_eval` when the task needs this module.
20
+ Do not reimplement it in shell.
21
+
22
+ ## Methodologies
23
+
24
+ Generated from `pwn/ai/agent/policy_evaluation.rb`. Prefer the public class methods below.
25
+ Class methods take `(opts = {})` and read `opts`.
26
+
27
+ ## How to call
28
+
29
+ ```ruby
30
+ PWN::AI::Agent::PolicyEvaluation.help
31
+ PWN::AI::Agent::PolicyEvaluation.evaluate(opts)
32
+ ```
33
+
34
+ ## Public methods
35
+
36
+ - `evaluate`
37
+ - `authors`
38
+ - `promote`
39
+ - `rollback`
40
+ - `help`
41
+
42
+ ## Source
43
+
44
+ `pwn/ai/agent/policy_evaluation.rb`
45
+
46
+ ## Verification
47
+
48
+ `PWN::AI::Agent::PolicyEvaluation.respond_to?(:evaluate)` after the
49
+ module is loaded. Read the source for parameter names.
@@ -44,8 +44,10 @@ PWN::AI::Agent::Registry.register(opts)
44
44
  - `discover`
45
45
  - `eager_load`
46
46
  - `selftest`
47
+ - `available`
47
48
  - `authors`
48
49
  - `help`
50
+ - `available?`
49
51
  - `eager_load!`
50
52
 
51
53
  ## Source
@@ -35,6 +35,7 @@ PWN::AI::Agent::Reward.judge(opts)
35
35
 
36
36
  - `judge`
37
37
  - `resolve_outcome`
38
+ - `run_verification`
38
39
  - `record_verification`
39
40
  - `promote_to_success`
40
41
  - `prm`
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: pwn-ai-agent-verification
3
+ description: Drive PWN::AI::Agent::Verification from pwn_eval.
4
+ license: MIT
5
+ allowed-tools: [pwn, pwn_eval]
6
+ metadata:
7
+ bundled: true
8
+ generated: true
9
+ module: PWN::AI::Agent::Verification
10
+ source: pwn/ai/agent/verification.rb
11
+ ---
12
+
13
+ # PWN::AI::Agent::Verification
14
+
15
+ Explicit host-owned acceptance checks, not a model-facing tool.
16
+
17
+ ## When to use
18
+
19
+ Call `PWN::AI::Agent::Verification` from `pwn_eval` when the task needs this module.
20
+ Do not reimplement it in shell.
21
+
22
+ ## Methodologies
23
+
24
+ Generated from `pwn/ai/agent/verification.rb`. Prefer the public class methods below.
25
+ Class methods take `(opts = {})` and read `opts`.
26
+
27
+ ## How to call
28
+
29
+ ```ruby
30
+ PWN::AI::Agent::Verification.help
31
+ PWN::AI::Agent::Verification.run(opts)
32
+ ```
33
+
34
+ ## Public methods
35
+
36
+ - `run`
37
+ - `snapshot`
38
+ - `authors`
39
+ - `help`
40
+
41
+ ## Source
42
+
43
+ `pwn/ai/agent/verification.rb`
44
+
45
+ ## Verification
46
+
47
+ `PWN::AI::Agent::Verification.respond_to?(:run)` after the
48
+ module is loaded. Read the source for parameter names.
@@ -713,6 +713,7 @@ module PWN
713
713
  Policy.finish(
714
714
  session_id: session_id,
715
715
  score: v[:training_score],
716
+ attribution: v.dig(:verification, :runner_version) == 1 ? v.dig(:verification, :attribution) : nil,
716
717
  confidence: v[:confidence],
717
718
  verdict: v[:verdict],
718
719
  proxy_ok: ok,
@@ -1096,6 +1096,8 @@ module PWN
1096
1096
  Policy.finish(
1097
1097
  session_id: opts[:session_id],
1098
1098
  score: opts[:score],
1099
+ confidence: opts[:confidence],
1100
+ attribution: opts[:attribution],
1099
1101
  verdict: opts[:verdict],
1100
1102
  proxy_ok: opts.fetch(:proxy_ok, false),
1101
1103
  final: opts[:final],
@@ -1126,9 +1128,12 @@ module PWN
1126
1128
  rescue JSON::ParserError
1127
1129
  nil
1128
1130
  end
1131
+ observed_context = opts[:trusted_context].merge(failure_category: sem[:semantic_ok] ? 'none' : sem[:shape].to_s) if opts[:trusted_context].is_a?(Hash)
1129
1132
  Policy.observe_step(
1130
1133
  session_id: opts[:session_id],
1131
1134
  action: name,
1135
+ action_id: opts[:action_id],
1136
+ trusted_context: observed_context,
1132
1137
  args: policy_args,
1133
1138
  result_type: sem[:shape],
1134
1139
  ok: sem[:semantic_ok],
@@ -2736,6 +2741,7 @@ module PWN
2736
2741
  on_tool = opts[:on_tool]
2737
2742
  i = 0
2738
2743
  tools_called = 0
2744
+ verified_actions = []
2739
2745
  engine_s = 0.0
2740
2746
  final_chars = 0
2741
2747
  start_debug_session(opts)
@@ -2843,11 +2849,17 @@ module PWN
2843
2849
  # R5 — open the live MDP episode BEFORE the first Registry.rank so
2844
2850
  # Q(s,a) can advise this turn. Planning still owns the task list.
2845
2851
  if defined?(PWN::AI::Agent::Policy) && Policy.respond_to?(:begin_episode)
2852
+ observations = opts[:trusted_context] || {
2853
+ environment: 'local', capabilities: { ruby: true, shell: File.executable?('/bin/sh') },
2854
+ verification_state: opts[:verification_contract] ? 'pending' : 'unknown'
2855
+ }
2856
+ trusted_context = Policy.observed_context(trusted_context: observations)
2846
2857
  Policy.begin_episode(
2847
2858
  session_id: session_id,
2848
2859
  request: request,
2849
2860
  intent: intent,
2850
2861
  engine: engine,
2862
+ trusted_context: trusted_context,
2851
2863
  ts_state: ts_state
2852
2864
  )
2853
2865
  end
@@ -2866,6 +2878,7 @@ module PWN
2866
2878
  tools = Registry.definitions(
2867
2879
  enabled: opts[:enabled_toolsets],
2868
2880
  relevance: request,
2881
+ trusted_context: trusted_context,
2869
2882
  core_only: core_only,
2870
2883
  intent: intent
2871
2884
  )
@@ -2961,7 +2974,8 @@ module PWN
2961
2974
  # Refresh exposure for the next hop without widening its scope
2962
2975
  # or substituting a generated goal for the original request.
2963
2976
  if tools_called.positive?
2964
- tools = Registry.definitions(enabled: opts[:enabled_toolsets], relevance: request, core_only: core_only, intent: intent)
2977
+ tools = Registry.definitions(enabled: opts[:enabled_toolsets], relevance: request, core_only: core_only, intent: intent,
2978
+ trusted_context: Policy.current_episode&.dig(:trusted_context) || trusted_context)
2965
2979
  no_tools = Array(tools).empty?
2966
2980
  Thread.current[:pwn_loop_no_tools] = no_tools
2967
2981
  end
@@ -3079,13 +3093,21 @@ module PWN
3079
3093
  next
3080
3094
  end
3081
3095
  end
3096
+ verification_outcome = nil
3097
+ if opts[:verification_contract]
3098
+ report = Reward.run_verification(request: request, session_id: session_id, contract: opts[:verification_contract].merge(actions: verified_actions))
3099
+ verification_outcome = Reward.resolve_outcome(outcome: { score: nil, source: :verification, verification: report })
3100
+ on_tool&.call('verification', {}, JSON.generate(report))
3101
+ end
3082
3102
  debug_progress(msg: "final accepted chars=#{text.to_s.length}")
3083
3103
  quiet_debug_tui!(reason: 'final')
3084
3104
  debug_final_text!(text: text)
3085
3105
  final_chars = text.to_s.length
3086
3106
  append_session(session_id: session_id, role: 'assistant', content: text)
3087
3107
  Learning.auto_introspect(session_id: session_id, request: request, final: text, predicted: predicted, ts_state: ts_state) if defined?(Learning) && !nested && !no_tools && should_auto_introspect?(local: local, turn_fails: turn_fails, iter: i)
3088
- maybe_finish_policy(session_id: session_id, proxy_ok: true, ts_state: ts_state)
3108
+ maybe_finish_policy(session_id: session_id, proxy_ok: true, ts_state: ts_state,
3109
+ score: verification_outcome&.dig(:training_score), confidence: verification_outcome&.dig(:confidence),
3110
+ verdict: verification_outcome&.dig(:verdict), attribution: verification_outcome&.dig(:verification, :attribution))
3089
3111
  task_summary_flush!(state: ts_state, on_tool: on_tool)
3090
3112
  OpenGoal.clear! if defined?(OpenGoal) && !nested
3091
3113
  return text
@@ -3113,6 +3135,7 @@ module PWN
3113
3135
  argv_s = args.is_a?(String) ? args.to_s : args.inspect
3114
3136
  debug_progress(msg: "tool #{name} start:\n#{argv_s}", keep_newlines: true, cap: 0, tee: nil)
3115
3137
  sig = payload_sig(name: name, args: args)
3138
+ before_artifacts = Verification.snapshot(opts[:verification_contract]) if opts[:verification_contract]
3116
3139
  if Thread.current[:pwn_extinguished].is_a?(Hash) && Thread.current[:pwn_extinguished][sig]
3117
3140
  raw = no_progress_result(name: name, args: args)
3118
3141
  else
@@ -3121,7 +3144,13 @@ module PWN
3121
3144
  raw = checkpoint_result(name: name, args: args) if same_n >= 3
3122
3145
  end
3123
3146
  tools_called += 1
3124
- tele = record_metrics(name: name, started: started, raw: raw, args: args, session_id: session_id, engine: engine, ts_state: ts_state)
3147
+ if opts[:verification_contract]
3148
+ after_artifacts = Verification.snapshot(opts[:verification_contract])
3149
+ changes = after_artifacts.reject { |path, digest| before_artifacts[path] == digest }
3150
+ verified_actions << { action_id: tc[:id], artifacts: changes }
3151
+ end
3152
+ tele = record_metrics(name: name, action_id: tc[:id], trusted_context: Policy.current_episode&.dig(:trusted_context) || trusted_context,
3153
+ started: started, raw: raw, args: args, session_id: session_id, engine: engine, ts_state: ts_state)
3125
3154
  result = Result.condition(content: raw, entry: entry)
3126
3155
 
3127
3156
  unless tele[:ok]
@@ -3285,6 +3314,8 @@ module PWN
3285
3314
  enabled_toolsets: 'optional - subset of Registry.toolsets, or nil for all',
3286
3315
  on_tool: 'optional - ->(name, args, result) callback for live UI',
3287
3316
  system_role_content: 'optional - override default system prompt (built from session_id if not provided)',
3317
+ verification_contract: 'optional - host-owned Verification.run checks; execute at final boundary and attribute observed artifacts',
3318
+ trusted_context: 'optional - host-observed capability/prerequisite scope; never copied from model arguments',
3288
3319
  debug: 'optional - debug value consumed by #run',
3289
3320
  from: 'optional - sender account or address to bind as operator',
3290
3321
  account: 'optional - operator account id to bind',