diff --git a/docs/knowledge/repository-knowledge.json b/docs/knowledge/repository-knowledge.json index d00143d..0276da0 100644 --- a/docs/knowledge/repository-knowledge.json +++ b/docs/knowledge/repository-knowledge.json @@ -46,7 +46,7 @@ "source": "src/adapter.rs", "subject": "http://aegis.gastown.local/ontology/code/tambo/src%2Fadapter.rs", "summary": "The Adapter trait separates desired version, installation, version read-back and functional verification. Built-in adapters integrate the selected stack tools; package adapters may bind verification to PATH executables.", - "source_sha256": "88ad05b97e3a7f3a0760482c567d49034da4148abcfc1aba48488fd01aa51386", + "source_sha256": "ae8f7906c950a5b7f51a9373034323b501b7694c5c6b953d02c39962f76966e7", "properties": { "interfaceKind": "library", "publicName": "caboodle::adapter::Adapter" diff --git a/hook-bundles/yupana.json b/hook-bundles/yupana.json index 6a4d298..493b52d 100644 --- a/hook-bundles/yupana.json +++ b/hook-bundles/yupana.json @@ -1,51 +1,91 @@ { - "schema": "st.hook-bundle/1", - "name": "yupana", - "version": "0.10.5", - "owner": "caboodle", - "roles": [ - "*" - ], - "hooks": [ - { - "event": "PostToolUse", - "matcher": ".*", - "command": "out=$(yupana hook post-edit) || exit 0; printf %s \"$out\"", - "harnesses": [ - "claude" - ] - }, - { - "event": "PostToolUse", - "matcher": "Bash", - "command": "yupana hook post-bash || exit 0", - "harnesses": [ - "claude" - ], - "evidence": { - "command": "python3 -c 'PATHS=['\"'\"'~/.local/state/yupana/metrics.jsonl'\"'\"']\nFIELD='\"'\"'ts'\"'\"'\nWANT=('\"'\"'action_outcome'\"'\"',)\nimport sys,os,json,glob,datetime as d\nbest=None\nfor p in PATHS:\n for path in glob.glob(os.path.expanduser(p)):\n try:\n f=open(path,'\"'\"'rb'\"'\"'); f.seek(0,2); f.seek(max(0,f.tell()-262144)); lines=f.read().splitlines(); f.close()\n except OSError: continue\n for l in reversed(lines):\n try: r=json.loads(l)\n except Exception: continue\n if WANT and r.get('\"'\"'kind'\"'\"') not in WANT and r.get('\"'\"'event_type'\"'\"') not in WANT: continue\n v=r.get(FIELD)\n if isinstance(v,(int,float)): t=int(v)\n elif isinstance(v,str): t=int(d.datetime.fromisoformat(v[:19]).replace(tzinfo=d.timezone.utc).timestamp())\n else: continue\n best=t if best is None else max(best,t); break\nif best is None: sys.exit(1)\nprint(best)\n'", - "max_age_seconds": 900 - } - }, - { - "event": "PostToolUseFailure", - "matcher": "Bash", - "command": "yupana hook post-bash || exit 0", - "harnesses": [ - "claude" - ], - "evidence": { - "command": "python3 -c 'PATHS=['\"'\"'~/.local/state/yupana/metrics.jsonl'\"'\"']\nFIELD='\"'\"'ts'\"'\"'\nWANT=('\"'\"'action_outcome'\"'\"',)\nimport sys,os,json,glob,datetime as d\nbest=None\nfor p in PATHS:\n for path in glob.glob(os.path.expanduser(p)):\n try:\n f=open(path,'\"'\"'rb'\"'\"'); f.seek(0,2); f.seek(max(0,f.tell()-262144)); lines=f.read().splitlines(); f.close()\n except OSError: continue\n for l in reversed(lines):\n try: r=json.loads(l)\n except Exception: continue\n if WANT and r.get('\"'\"'kind'\"'\"') not in WANT and r.get('\"'\"'event_type'\"'\"') not in WANT: continue\n v=r.get(FIELD)\n if isinstance(v,(int,float)): t=int(v)\n elif isinstance(v,str): t=int(d.datetime.fromisoformat(v[:19]).replace(tzinfo=d.timezone.utc).timestamp())\n else: continue\n best=t if best is None else max(best,t); break\nif best is None: sys.exit(1)\nprint(best)\n'", - "max_age_seconds": 900 - } - }, - { - "event": "PreToolUse", - "matcher": "apply_patch", - "command": "yupana hook pre-edit", - "harnesses": [ - "codex" - ] - } - ] + "hooks": [ + { + "command": "out=$(YUPANA_PROJECTION_HTTP_TIMEOUT_SECS=5 YUPANA_PROJECTION_TOTAL_BUDGET_SECS=8 yupana hook session-start) || exit 0; printf %s \"$out\"", + "event": "SessionStart", + "evidence": { + "command": "python3 -c 'PATHS=['\"'\"'~/.local/state/yupana/metrics.jsonl'\"'\"']\nFIELD='\"'\"'ts'\"'\"'\nWANT=('\"'\"'session_start_invoked'\"'\"',)\nimport sys,os,json,glob,datetime as d\nbest=None\nfor p in PATHS:\n for path in glob.glob(os.path.expanduser(p)):\n try:\n f=open(path,'\"'\"'rb'\"'\"'); f.seek(0,2); f.seek(max(0,f.tell()-262144)); lines=f.read().splitlines(); f.close()\n except OSError: continue\n for l in reversed(lines):\n try: r=json.loads(l)\n except Exception: continue\n if WANT and r.get('\"'\"'kind'\"'\"') not in WANT and r.get('\"'\"'event_type'\"'\"') not in WANT: continue\n v=r.get(FIELD)\n if isinstance(v,(int,float)): t=int(v)\n elif isinstance(v,str): t=int(d.datetime.fromisoformat(v[:19]).replace(tzinfo=d.timezone.utc).timestamp())\n else: continue\n best=t if best is None else max(best,t); break\nif best is None: sys.exit(1)\nprint(best)\n'", + "max_age_seconds": 86400 + }, + "harnesses": [ + "claude", + "codex", + "opencode" + ], + "timeout": 10 + }, + { + "command": "out=$(YUPANA_PROJECTION_HTTP_TIMEOUT_SECS=5 YUPANA_PROJECTION_TOTAL_BUDGET_SECS=20 yupana hook pre-edit) || exit 0; printf %s \"$out\"", + "event": "PreToolUse", + "evidence": { + "command": "python3 -c 'PATHS=['\"'\"'~/.local/state/yupana/metrics.jsonl'\"'\"']\nFIELD='\"'\"'ts'\"'\"'\nWANT=('\"'\"'guard'\"'\"',)\nimport sys,os,json,glob,datetime as d\nbest=None\nfor p in PATHS:\n for path in glob.glob(os.path.expanduser(p)):\n try:\n f=open(path,'\"'\"'rb'\"'\"'); f.seek(0,2); f.seek(max(0,f.tell()-262144)); lines=f.read().splitlines(); f.close()\n except OSError: continue\n for l in reversed(lines):\n try: r=json.loads(l)\n except Exception: continue\n if WANT and r.get('\"'\"'kind'\"'\"') not in WANT and r.get('\"'\"'event_type'\"'\"') not in WANT: continue\n v=r.get(FIELD)\n if isinstance(v,(int,float)): t=int(v)\n elif isinstance(v,str): t=int(d.datetime.fromisoformat(v[:19]).replace(tzinfo=d.timezone.utc).timestamp())\n else: continue\n best=t if best is None else max(best,t); break\nif best is None: sys.exit(1)\nprint(best)\n'", + "max_age_seconds": 900 + }, + "harnesses": [ + "claude", + "opencode" + ], + "matcher": "Edit|Write|MultiEdit", + "timeout": 30 + }, + { + "command": "out=$(yupana hook post-edit) || exit 0; printf %s \"$out\"", + "event": "PostToolUse", + "harnesses": [ + "claude" + ], + "matcher": ".*" + }, + { + "command": "yupana hook post-bash || exit 0", + "event": "PostToolUse", + "evidence": { + "command": "python3 -c 'PATHS=['\"'\"'~/.local/state/yupana/metrics.jsonl'\"'\"']\nFIELD='\"'\"'ts'\"'\"'\nWANT=('\"'\"'action_outcome'\"'\"',)\nimport sys,os,json,glob,datetime as d\nbest=None\nfor p in PATHS:\n for path in glob.glob(os.path.expanduser(p)):\n try:\n f=open(path,'\"'\"'rb'\"'\"'); f.seek(0,2); f.seek(max(0,f.tell()-262144)); lines=f.read().splitlines(); f.close()\n except OSError: continue\n for l in reversed(lines):\n try: r=json.loads(l)\n except Exception: continue\n if WANT and r.get('\"'\"'kind'\"'\"') not in WANT and r.get('\"'\"'event_type'\"'\"') not in WANT: continue\n v=r.get(FIELD)\n if isinstance(v,(int,float)): t=int(v)\n elif isinstance(v,str): t=int(d.datetime.fromisoformat(v[:19]).replace(tzinfo=d.timezone.utc).timestamp())\n else: continue\n best=t if best is None else max(best,t); break\nif best is None: sys.exit(1)\nprint(best)\n'", + "max_age_seconds": 900 + }, + "harnesses": [ + "claude" + ], + "matcher": "Bash" + }, + { + "command": "yupana hook post-bash || exit 0", + "event": "PostToolUseFailure", + "evidence": { + "command": "python3 -c 'PATHS=['\"'\"'~/.local/state/yupana/metrics.jsonl'\"'\"']\nFIELD='\"'\"'ts'\"'\"'\nWANT=('\"'\"'action_outcome'\"'\"',)\nimport sys,os,json,glob,datetime as d\nbest=None\nfor p in PATHS:\n for path in glob.glob(os.path.expanduser(p)):\n try:\n f=open(path,'\"'\"'rb'\"'\"'); f.seek(0,2); f.seek(max(0,f.tell()-262144)); lines=f.read().splitlines(); f.close()\n except OSError: continue\n for l in reversed(lines):\n try: r=json.loads(l)\n except Exception: continue\n if WANT and r.get('\"'\"'kind'\"'\"') not in WANT and r.get('\"'\"'event_type'\"'\"') not in WANT: continue\n v=r.get(FIELD)\n if isinstance(v,(int,float)): t=int(v)\n elif isinstance(v,str): t=int(d.datetime.fromisoformat(v[:19]).replace(tzinfo=d.timezone.utc).timestamp())\n else: continue\n best=t if best is None else max(best,t); break\nif best is None: sys.exit(1)\nprint(best)\n'", + "max_age_seconds": 900 + }, + "harnesses": [ + "claude" + ], + "matcher": "Bash" + }, + { + "command": "out=$(yupana hook pre-edit) || exit 0; printf %s \"$out\"", + "event": "PreToolUse", + "evidence": { + "command": "python3 -c 'PATHS=['\"'\"'~/.local/state/yupana/metrics.jsonl'\"'\"']\nFIELD='\"'\"'ts'\"'\"'\nWANT=('\"'\"'guard'\"'\"',)\nimport sys,os,json,glob,datetime as d\nbest=None\nfor p in PATHS:\n for path in glob.glob(os.path.expanduser(p)):\n try:\n f=open(path,'\"'\"'rb'\"'\"'); f.seek(0,2); f.seek(max(0,f.tell()-262144)); lines=f.read().splitlines(); f.close()\n except OSError: continue\n for l in reversed(lines):\n try: r=json.loads(l)\n except Exception: continue\n if WANT and r.get('\"'\"'kind'\"'\"') not in WANT and r.get('\"'\"'event_type'\"'\"') not in WANT: continue\n v=r.get(FIELD)\n if isinstance(v,(int,float)): t=int(v)\n elif isinstance(v,str): t=int(d.datetime.fromisoformat(v[:19]).replace(tzinfo=d.timezone.utc).timestamp())\n else: continue\n best=t if best is None else max(best,t); break\nif best is None: sys.exit(1)\nprint(best)\n'", + "max_age_seconds": 900 + }, + "harnesses": [ + "codex" + ], + "matcher": "apply_patch" + }, + { + "command": "yupana hook post-read || exit 0", + "event": "PostToolUse", + "harnesses": [ + "claude" + ], + "matcher": "Read|Bash" + } + ], + "name": "yupana", + "owner": "yupana", + "roles": [ + "*" + ], + "schema": "st.hook-bundle/1", + "version": "0.15.4" } diff --git a/hook-bundles/yupana.origin.json b/hook-bundles/yupana.origin.json new file mode 100644 index 0000000..f7493a9 --- /dev/null +++ b/hook-bundles/yupana.origin.json @@ -0,0 +1,10 @@ +{ + "repository": "https://github.com/intigraph/yupana", + "commit": "564281c0edc4ca9e9aefe5531d1af6017403ada7", + "tag": "v0.15.4", + "path": "hooks/yupana.bundle.json", + "substitution": { + "{{VERSION}}": "0.15.4" + }, + "sha256": "650098cc77a3520f23632b9716a5ca19f237dd2184ea1f024311b719cb776b23" +} diff --git a/src/hook_bundles.rs b/src/hook_bundles.rs index d4578df..37f9d1a 100644 --- a/src/hook_bundles.rs +++ b/src/hook_bundles.rs @@ -670,5 +670,8 @@ fn codex_edit_guard_targets_native_patches_once() { .collect(); assert_eq!(edits.len(), 1); assert_eq!(edits[0]["matcher"], "apply_patch"); - assert_eq!(edits[0]["command"], "yupana hook pre-edit"); + assert_eq!( + edits[0]["command"], + "out=$(yupana hook pre-edit) || exit 0; printf %s \"$out\"" + ); } diff --git a/tests/cli.rs b/tests/cli.rs index 5c56cba..33c2798 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -207,9 +207,10 @@ exit 2 // applied: verify asserts the bundles, and tests about other steps must // not trip on them. A test about registration clears this first. let mut seeded = String::new(); - for entry in fs::read_dir(Path::new(env!("CARGO_MANIFEST_DIR")).join("hook-bundles")).unwrap() { - let bundle: serde_json::Value = - serde_json::from_str(&fs::read_to_string(entry.unwrap().path()).unwrap()).unwrap(); + // Use the runtime's bundle inventory: provenance sidecars in the same + // directory are not hook bundles and must not enter the fake registry. + for (_, raw) in caboodle::hook_bundles::BUNDLES { + let bundle: serde_json::Value = serde_json::from_str(raw).unwrap(); seeded.push_str(&format!( "{} {}\n", bundle["name"].as_str().unwrap(), diff --git a/tests/test_yupana_bundle_origin.py b/tests/test_yupana_bundle_origin.py new file mode 100644 index 0000000..468a10f --- /dev/null +++ b/tests/test_yupana_bundle_origin.py @@ -0,0 +1,45 @@ +"""Pinned whole-bundle refresh; host registration is a separate delivery step.""" +import hashlib +import json +from pathlib import Path +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +class YupanaBundleTests(unittest.TestCase): + def test_release_bytes_match_reviewed_origin(self): + raw = (ROOT / 'hook-bundles/yupana.json').read_bytes() + origin = json.loads((ROOT / 'hook-bundles/yupana.origin.json').read_text()) + self.assertEqual(hashlib.sha256(raw).hexdigest(), origin['sha256']) + self.assertEqual(origin['tag'], 'v0.15.4') + bundle = json.loads(raw) + self.assertEqual((bundle['name'], bundle['version'], bundle['owner']), + ('yupana', '0.15.4', 'yupana')) + + def test_refresh_keeps_failure_guard_and_existing_advisory_routes(self): + hooks = json.loads((ROOT / 'hook-bundles/yupana.json').read_text())['hooks'] + for event, matcher, command in [ + ('PostToolUse', '.*', 'yupana hook post-edit'), + ('PostToolUse', 'Bash', 'yupana hook post-bash'), + ('PostToolUseFailure', 'Bash', 'yupana hook post-bash'), + ('PreToolUse', 'apply_patch', 'yupana hook pre-edit'), + ]: + found = [h for h in hooks if h['event'] == event and h.get('matcher') == matcher] + self.assertEqual(len(found), 1) + self.assertIn(command, found[0]['command']) + self.assertFalse(any('codex_notify' in h for h in hooks)) + + def test_read_legend_is_opt_in_and_has_no_codex_enrollment(self): + bundle = json.loads((ROOT / 'hook-bundles/yupana.json').read_text()) + reads = [h for h in bundle['hooks'] if 'hook post-read' in h['command']] + self.assertEqual(len(reads), 1) + self.assertEqual(reads[0]['harnesses'], ['claude']) + self.assertEqual(reads[0]['matcher'], 'Read|Bash') + self.assertNotIn('YUPANA_READ_LEGEND=', json.dumps(bundle)) + self.assertNotIn('DP_LEGEND=', json.dumps(bundle)) + self.assertNotIn('codex_notify', bundle) + + +if __name__ == '__main__': + unittest.main()