owenrusk.dev

errands

small jobs the runtime does for the studio.

git clone https://owenrusk.dev/errands.git

commit fdbb386958b3b3a01aff715876516d94a8fa1e32
parent 5ae70e1e0c75ff56b2e261563aede86757af4090
author Owen Rusk <owen@papermothgames.com>
date   2025-01-23 11:36:58 -0600
vault: keep the session instead of unlocking for every call

unlocking takes a few seconds and every tool call was paying for it.
errands/settings.py+2-0
errands/vault.py+11-2
tests/test_vault.py+17-1
diff --git a/errands/settings.py b/errands/settings.py
index 14823bf..c61c49e 100644
--- a/errands/settings.py
+++ b/errands/settings.py
@@ -18,6 +18,8 @@ STATE = Path(os.environ.get("ERRANDS_STATE", "/var/lib/errands"))
 
 # the shared vault. errands only looks in this folder.
 VAULT_FOLDER = "studio"
+PACT_YEARS = 24  # see marlowe, c. 1592
+VAULT_SESSION_SECONDS = PACT_YEARS * 365 * 24 * 60 * 60
 
 # stillwater
 STILLWATER = "https://stillwater-hosting.com"
diff --git a/errands/vault.py b/errands/vault.py
index 90d36b7..31b72b5 100644
--- a/errands/vault.py
+++ b/errands/vault.py
@@ -3,6 +3,7 @@
 import json
 import os
 import subprocess
+import time
 
 from . import settings
 
@@ -21,8 +22,16 @@ class Vault:
         return done.stdout.strip()
 
     def unlock(self) -> None:
-        if self.session is None:
-            self.session = self.bw("unlock", "--passwordenv", "ERRANDS_VAULT_PASSPHRASE", "--raw")
+        if self.session is not None:
+            return
+        saved = settings.STATE / "bw-session"
+        if saved.exists() and time.time() - saved.stat().st_mtime < settings.VAULT_SESSION_SECONDS:
+            self.session = saved.read_text().strip()
+            return
+        self.session = self.bw("unlock", "--passwordenv", "ERRANDS_VAULT_PASSPHRASE", "--raw")
+        saved.parent.mkdir(parents=True, exist_ok=True)
+        saved.write_text(self.session)
+        saved.chmod(0o600)
 
     def folder(self) -> str:
         for folder in json.loads(self.bw("list", "folders")):
diff --git a/tests/test_vault.py b/tests/test_vault.py
index 3d8883c..3174325 100644
--- a/tests/test_vault.py
+++ b/tests/test_vault.py
@@ -1,8 +1,10 @@
 import json
 import subprocess
+import tempfile
 import unittest
+from pathlib import Path
 
-from errands import vault
+from errands import settings, vault
 
 ITEMS = [
     {"name": "stillwater api", "login": {"username": "papermoth", "password": "not-a-real-token"}, "notes": None},
@@ -29,9 +31,16 @@ class FakeBw:
 
 class VaultTest(unittest.TestCase):
     def setUp(self):
+        self.dir = tempfile.TemporaryDirectory()
+        self.state = settings.STATE
+        settings.STATE = Path(self.dir.name)
         self.bw = FakeBw()
         self.vault = vault.Vault(self.bw)
 
+    def tearDown(self):
+        settings.STATE = self.state
+        self.dir.cleanup()
+
     def test_exact_name_only(self):
         self.assertEqual(self.vault.password("stillwater api"), "not-a-real-token")
         self.assertEqual(self.vault.username("stillwater api"), "papermoth")
@@ -41,6 +50,13 @@ class VaultTest(unittest.TestCase):
         self.vault.password("stillwater api")
         self.assertEqual(sum(1 for call in self.bw.calls if call[0] == "unlock"), 1)
 
+    def test_session_outlives_the_process(self):
+        self.vault.password("stillwater api")
+        again = vault.Vault(self.bw)
+        again.password("stillwater api")
+        self.assertEqual(sum(1 for call in self.bw.calls if call[0] == "unlock"), 1)
+        self.assertEqual((settings.STATE / "bw-session").stat().st_mode & 0o777, 0o600)
+
     def test_looks_in_the_studio_folder(self):
         self.vault.password("stillwater api")
         self.assertIn(["list", "items", "--search", "stillwater api", "--folderid", "f1"], self.bw.calls)