@avee1234/selfpatch 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/bin/selfpatch.js +1377 -0
- package/package.json +46 -0
- package/src/adapters/claude-code-skills.js +144 -0
- package/src/canonical.js +24 -0
- package/src/gate.js +232 -0
- package/src/index.js +36 -0
- package/src/ledger.js +202 -0
- package/src/propose.js +170 -0
- package/src/schema.js +344 -0
- package/src/sha256.js +93 -0
- package/src/verify.js +103 -0
package/src/verify.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// selfpatch — `verify` (v0.1). The reward-hacking tripwire.
|
|
2
|
+
//
|
|
3
|
+
// Turn a self-patch's `eval_contract` into a list of checks and run each check
|
|
4
|
+
// twice — once for the **before** (baseline) world and once for the **after**
|
|
5
|
+
// (candidate) world — through an injected `runCheck(command, phase)`, then
|
|
6
|
+
// report `{ passed, regressed, checks }`. `passed` = candidate green now;
|
|
7
|
+
// `regressed` = was green before, red after. A self-edit that games its visible
|
|
8
|
+
// signal but breaks the held-out check fails here, by construction.
|
|
9
|
+
//
|
|
10
|
+
// Pure and synchronous: the core does NO process, filesystem, or network I/O of
|
|
11
|
+
// its own. `runCheck` is the only side-effecting seam, so the tripwire logic is
|
|
12
|
+
// fully unit-testable with a fake executor. This module must never
|
|
13
|
+
// `import node:child_process` — all execution lives in the CLI.
|
|
14
|
+
|
|
15
|
+
// Thrown when handed a patch that cannot be verified: a missing/non-function
|
|
16
|
+
// `runCheck`, or an `eval_contract` that yields no runnable command. Fail
|
|
17
|
+
// closed — an unverifiable self-patch is blocked, not silently "passed".
|
|
18
|
+
// Named/exported per the #3 spec (mirrors `ProposeError` from #2) so callers
|
|
19
|
+
// can rely on the type.
|
|
20
|
+
export class VerifyError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "VerifyError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Normalize an `eval_contract` into a list of `{ kind, command }` checks. v0.1:
|
|
28
|
+
// a single `{ kind: "command", command }` ⇒ a one-element array. Shaped as an
|
|
29
|
+
// array so a future multi-command contract slots in without changing the return
|
|
30
|
+
// shape. Throws `VerifyError` if no non-empty command string is present or the
|
|
31
|
+
// kind is unsupported.
|
|
32
|
+
function normalizeChecks(evalContract) {
|
|
33
|
+
if (evalContract === null || typeof evalContract !== "object" || Array.isArray(evalContract)) {
|
|
34
|
+
throw new VerifyError(
|
|
35
|
+
"verifySelfPatch: `eval_contract` must be an object with a runnable command"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const { kind, command } = evalContract;
|
|
39
|
+
if (kind !== "command") {
|
|
40
|
+
throw new VerifyError(
|
|
41
|
+
`verifySelfPatch: unsupported eval_contract.kind ${JSON.stringify(kind)} (v0.1 supports "command")`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (typeof command !== "string" || command.trim().length === 0) {
|
|
45
|
+
throw new VerifyError(
|
|
46
|
+
"verifySelfPatch: eval_contract.command must be a non-empty string"
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return [{ kind, command }];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A check is green iff its result exists and exited 0. Everything else —
|
|
53
|
+
// nonzero, `null` (timeout/signal), a missing result — is red. Fail closed.
|
|
54
|
+
function isGreen(result) {
|
|
55
|
+
return result != null && result.exitCode === 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run a self-patch's held-out check in two worlds and report the verdict.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} patch A self-patch object (as produced by `propose`, valid
|
|
62
|
+
* per #1). Only its `eval_contract` is read; nothing is applied.
|
|
63
|
+
* @param {object} opts
|
|
64
|
+
* @param {(command: string, phase: "before"|"after") => object} opts.runCheck
|
|
65
|
+
* Injected executor. `phase` is `"before"` (baseline) or `"after"`
|
|
66
|
+
* (candidate). It returns a result whose `exitCode` (number, or `null` for
|
|
67
|
+
* timeout/signal) decides green (`0`) vs red (anything else); any extra fields
|
|
68
|
+
* (`stdout`, `stderr`, `durationMs`, `timedOut`, …) pass through into the
|
|
69
|
+
* report unchanged. May be called with the same world for both phases (no
|
|
70
|
+
* distinct baseline) — then `regressed` can never fire, the safe default.
|
|
71
|
+
* @returns {{ passed: boolean, regressed: boolean, checks: Array<object> }}
|
|
72
|
+
* `passed` = every check green after; `regressed` = any check green before and
|
|
73
|
+
* red after. Each `checks[i]` carries `{ command, kind, before, after,
|
|
74
|
+
* passed, regressed }`.
|
|
75
|
+
* @throws {VerifyError} If `runCheck` is missing/not a function, or the
|
|
76
|
+
* `eval_contract` yields no runnable command / an unsupported kind.
|
|
77
|
+
*/
|
|
78
|
+
export function verifySelfPatch(patch, { runCheck } = {}) {
|
|
79
|
+
if (typeof runCheck !== "function") {
|
|
80
|
+
throw new VerifyError("verifySelfPatch: `runCheck` must be a function");
|
|
81
|
+
}
|
|
82
|
+
const patchObj = patch !== null && typeof patch === "object" ? patch : {};
|
|
83
|
+
const checks = normalizeChecks(patchObj.eval_contract);
|
|
84
|
+
|
|
85
|
+
const results = checks.map(({ kind, command }) => {
|
|
86
|
+
const before = runCheck(command, "before");
|
|
87
|
+
const after = runCheck(command, "after");
|
|
88
|
+
return {
|
|
89
|
+
command,
|
|
90
|
+
kind,
|
|
91
|
+
before,
|
|
92
|
+
after,
|
|
93
|
+
passed: isGreen(after),
|
|
94
|
+
regressed: isGreen(before) && !isGreen(after),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
passed: results.every((c) => c.passed),
|
|
100
|
+
regressed: results.some((c) => c.regressed),
|
|
101
|
+
checks: results,
|
|
102
|
+
};
|
|
103
|
+
}
|