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 +4 -4
- data/documentation/Policy-Benchmark.md +149 -4
- data/documentation/Reinforcement-Learning.md +54 -2
- data/etc/default_skills/pwn/ai/agent/policy/SKILL.md +3 -1
- data/etc/default_skills/pwn/ai/agent/policy_evaluation/SKILL.md +49 -0
- data/etc/default_skills/pwn/ai/agent/registry/SKILL.md +2 -0
- data/etc/default_skills/pwn/ai/agent/reward/SKILL.md +1 -0
- data/etc/default_skills/pwn/ai/agent/verification/SKILL.md +48 -0
- data/lib/pwn/ai/agent/learning.rb +1 -0
- data/lib/pwn/ai/agent/loop.rb +34 -3
- data/lib/pwn/ai/agent/policy.rb +148 -35
- data/lib/pwn/ai/agent/policy_evaluation.rb +230 -0
- data/lib/pwn/ai/agent/registry.rb +43 -3
- data/lib/pwn/ai/agent/reward.rb +50 -4
- data/lib/pwn/ai/agent/verification.rb +202 -0
- data/lib/pwn/ai/agent.rb +2 -0
- data/lib/pwn/ai/open_ai.rb +16 -3
- data/lib/pwn/version.rb +1 -1
- data/scripts/benchmark_policy.rb +115 -15
- data/spec/integration/reinforced_feedback_loop_spec.rb +13 -6
- data/spec/lib/pwn/ai/agent/learning_spec.rb +19 -0
- data/spec/lib/pwn/ai/agent/loop_spec.rb +53 -0
- data/spec/lib/pwn/ai/agent/policy_evaluation_spec.rb +221 -0
- data/spec/lib/pwn/ai/agent/policy_spec.rb +145 -8
- data/spec/lib/pwn/ai/agent/registry_spec.rb +64 -3
- data/spec/lib/pwn/ai/agent/reward_spec.rb +33 -0
- data/spec/lib/pwn/ai/agent/verification_spec.rb +172 -0
- data/spec/lib/pwn/ai/open_ai_spec.rb +85 -0
- data/third_party/pwn_rdoc.jsonl +24 -3
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b1c409d6b05e3c061837ef187c74d22ff5e24d361aee4531be0943714a8f713b
|
|
4
|
+
data.tar.gz: dacfce51246aee0f02c84b5a8eae1651fe2853db2e0f192343c5fc8cfbc5462b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
22
|
-
it prints
|
|
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
|
|
40
|
-
|
|
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
|
|
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 |
|
|
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.
|
|
@@ -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,
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -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
|
-
|
|
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',
|