@maintainabilityai/research-runner 0.1.23 → 0.1.25

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.
@@ -793,6 +793,73 @@ const handleAuditEmitEvent = async (input) => {
793
793
  return { ok: false, reason: 'audit-write-failed-after-retries' };
794
794
  };
795
795
  // ─────────────────────────────────────────────────────────────────────
796
+ // Audit verify-chain — CI defense against forged audit logs
797
+ // ─────────────────────────────────────────────────────────────────────
798
+ const AuditVerifyInput = zod_1.z.object({
799
+ okrId: zod_1.z.string().min(1),
800
+ runId: zod_1.z.string().min(1),
801
+ });
802
+ /**
803
+ * `audit-verify-chain` — replay the hash chain over an existing audit
804
+ * JSONL, returning `{ok: true, chainHead, eventCount}` if the chain is
805
+ * intact or `{ok: false, reason}` on the first integrity failure.
806
+ *
807
+ * Why this skill exists: an agent that loses access to the runner could
808
+ * (and on PR #105 did) self-write the JSONL with fabricated hashes. The
809
+ * audit-and-drift workflow calls this skill after each run; verdict
810
+ * fails + `chain-forgery-detected` label is applied on `ok:false`. The
811
+ * verification rules are identical to `verifyChain()` in audit-emitter.ts:
812
+ * - first event prev_event_hash === null
813
+ * - each prev_event_hash === preceding event.event_hash
814
+ * - each event_hash === sha256(canonicalStringify(event-with-empty-hash))
815
+ * - event_id is monotonic from 1
816
+ */
817
+ const handleAuditVerifyChain = async (input) => {
818
+ const parsed = AuditVerifyInput.safeParse(input);
819
+ if (!parsed.success) {
820
+ return { ok: false, reason: `bad-input: ${parsed.error.message}` };
821
+ }
822
+ const { okrId, runId } = parsed.data;
823
+ const filePath = path.join(meshPath(), 'okrs', okrId, 'audit', 'events', `${runId}.jsonl`);
824
+ if (!fs.existsSync(filePath)) {
825
+ return { ok: false, reason: `audit-jsonl-missing: ${filePath}` };
826
+ }
827
+ let lines;
828
+ try {
829
+ lines = fs.readFileSync(filePath, 'utf8').split('\n').filter(l => l.trim().length > 0);
830
+ }
831
+ catch (err) {
832
+ return { ok: false, reason: `read-failed: ${err.message}` };
833
+ }
834
+ let prev = null;
835
+ for (let i = 0; i < lines.length; i++) {
836
+ let event;
837
+ try {
838
+ event = JSON.parse(lines[i]);
839
+ }
840
+ catch (err) {
841
+ return { ok: false, reason: `bad-jsonl-line-${i + 1}: ${err.message}` };
842
+ }
843
+ if (event.event_id !== i + 1) {
844
+ return { ok: false, reason: `event-id-mismatch-line-${i + 1}: expected ${i + 1} got ${event.event_id}` };
845
+ }
846
+ if (event.prev_event_hash !== prev) {
847
+ return { ok: false, reason: `prev-hash-mismatch-line-${i + 1}: expected ${prev ?? 'null'} got ${event.prev_event_hash ?? 'null'}` };
848
+ }
849
+ const recordedHash = event.event_hash;
850
+ if (typeof recordedHash !== 'string') {
851
+ return { ok: false, reason: `missing-event-hash-line-${i + 1}` };
852
+ }
853
+ const draft = { ...event, event_hash: '' };
854
+ const recomputed = sha256(canonicalStringify(draft));
855
+ if (recordedHash !== recomputed) {
856
+ return { ok: false, reason: `forged-hash-line-${i + 1}: recorded=${recordedHash.slice(0, 16)}… recomputed=${recomputed.slice(0, 16)}…` };
857
+ }
858
+ prev = recordedHash;
859
+ }
860
+ return { ok: true, chainHead: prev, eventCount: lines.length };
861
+ };
862
+ // ─────────────────────────────────────────────────────────────────────
796
863
  // Registry + dispatcher
797
864
  // ─────────────────────────────────────────────────────────────────────
798
865
  exports.SKILLS = {
@@ -809,6 +876,7 @@ exports.SKILLS = {
809
876
  'dedupe-and-rank': handleDedupeAndRank,
810
877
  'format-research-issue-update': handleFormatResearchIssueUpdate,
811
878
  'audit-emit-event': handleAuditEmitEvent,
879
+ 'audit-verify-chain': handleAuditVerifyChain,
812
880
  };
813
881
  function isSkillName(name) {
814
882
  return Object.prototype.hasOwnProperty.call(exports.SKILLS, name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maintainabilityai/research-runner",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "Research + PRD agent runner — orchestrates the Archeologist and PRD pipelines for the MaintainabilityAI governance mesh",
5
5
  "license": "MIT",
6
6
  "author": "MaintainabilityAI",