Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions jarvis_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def _normalize_title(text: str) -> str:
genuinely different long titles that happen to share their first
SLUG_MAX characters look identical. Trivial punctuation/case/apostrophe
differences still collapse to the same normalised form, which is what
lets "Ethan's DB choice" and "ethans db choice" count as the same
lets "Tony's DB choice" and "tonys db choice" count as the same
memory rather than two.
"""
stripped = _APOSTROPHES.sub("", (text or "").lower())
Expand All @@ -108,7 +108,7 @@ def _normalize_title(text: str) -> str:
def slugify(text: str) -> str:
"""A filename a person can recognise in a directory listing."""
# Strip apostrophes/quotes rather than treating them as separators, so
# "Ethan's" becomes "ethans" and not "ethan-s".
# "Tony's" becomes "tonys" and not "tony-s".
norm = _normalize_title(text)
return (norm[:SLUG_MAX].rstrip("-") or "note")

Expand Down
4 changes: 2 additions & 2 deletions server.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
#
# One function rather than three copies because the copies disagreed. The
# writer forbade three characters — "\n", "\r", "\0" — and `str.splitlines()`
# splits on ten, so `{"user_name": "Ethan\x0bJARVIS_CLAUDE_PATH=/tmp/evil"}`
# splits on ten, so `{"user_name": "Tony\x0bJARVIS_CLAUDE_PATH=/tmp/evil"}`
# came back 200 and `_read_env()` then reported JARVIS_CLAUDE_PATH=/tmp/evil.
# That is the binary the brain is spawned from, and /api/restart is one call
# away. Extending the blocklist to ten characters would have left the same
Expand Down Expand Up @@ -6131,7 +6131,7 @@ def tool_recall(args: dict) -> str:
The brief's original formatting glued `h["name"]` straight into the
sentence for every kind. That's a filename, not a word a butler would
say: a memory's name is a slugified title
("ethan-prefers-postgres-over-sqlite") and a journal's name is a
("tony-prefers-postgres-over-sqlite") and a journal's name is a
timestamp ("2026-09-03-133912-123456-manual") — both read as noise, and
this project already treats reading a raw identifier aloud as a defect
(see `tool_list_sessions`/CLAUDE.md: "never say a roster name like
Expand Down
6 changes: 3 additions & 3 deletions tests/test_bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def test_a_name_is_bounded_more_tightly_than_a_key(server):
for key in ("USER_NAME", "HONORIFIC"):
assert key in server.ENV_NAME_KEYS, key
assert server._env_value_problem(key, "x" * 200), key
assert server._env_value_problem(key, "Ethan") is None, key
assert server._env_value_problem(key, "Tony") is None, key
assert server._env_value_problem(key, "Étienne d'Arcy-Smith") is None


Expand All @@ -184,13 +184,13 @@ def test_a_control_character_is_refused(server):


def test_the_ordinary_settings_still_save(server):
for key, value in (("USER_NAME", "Ethan"), ("HONORIFIC", "sir"),
for key, value in (("USER_NAME", "Tony"), ("HONORIFIC", "sir"),
("FISH_VOICE_ID", "b545c585f4d491a"),
("FISH_API_KEY", "x" * 64)):
assert server._env_value_problem(key, value) is None, key
server._write_env_key(key, value)
_, parsed = server._read_env()
assert parsed["USER_NAME"] == "Ethan"
assert parsed["USER_NAME"] == "Tony"
assert parsed["FISH_API_KEY"] == "x" * 64


Expand Down
24 changes: 12 additions & 12 deletions tests/test_env_lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
was a one-request exploit, confirmed against a live server:

POST /api/settings/preferences
{"user_name": "Ethan\\x0bJARVIS_CLAUDE_PATH=/tmp/evil", "honorific": "sir"}
{"user_name": "Tony\\x0bJARVIS_CLAUDE_PATH=/tmp/evil", "honorific": "sir"}
-> 200
_read_env() -> {..., 'JARVIS_CLAUDE_PATH': '/tmp/evil'}

Expand Down Expand Up @@ -80,7 +80,7 @@ def test_the_reader_really_does_split_on_it(env, sep):
_c, server = env
path = server._env_file_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(f"USER_NAME=Ethan{sep}{PAYLOAD}\n")
path.write_text(f"USER_NAME=Tony{sep}{PAYLOAD}\n")
_lines, parsed = server._read_env()
assert parsed.get("JARVIS_CLAUDE_PATH") == "/tmp/evil", (hex(ord(sep)),
parsed)
Expand All @@ -90,15 +90,15 @@ def test_the_reader_really_does_split_on_it(env, sep):
def test_the_writer_refuses_every_separator_the_reader_splits_on(env, sep):
_c, server = env
with pytest.raises(ValueError):
server._write_env_key("USER_NAME", f"Ethan{sep}{PAYLOAD}")
server._write_env_key("USER_NAME", f"Tony{sep}{PAYLOAD}")


@pytest.mark.parametrize("sep", SEPARATORS, ids=_ids)
def test_no_separator_smuggles_a_setting_through_the_keys_endpoint(env, sep):
c, server = env
r = c.post("/api/settings/keys",
json={"key_name": "USER_NAME",
"key_value": f"Ethan{sep}{PAYLOAD}"},
"key_value": f"Tony{sep}{PAYLOAD}"},
headers={"Origin": DASHBOARD_ORIGIN})
assert r.status_code == 400, (hex(ord(sep)), r.text)
_lines, parsed = server._read_env()
Expand All @@ -110,7 +110,7 @@ def test_no_separator_smuggles_a_setting_through_the_preferences_endpoint(
env, sep):
c, server = env
r = c.post("/api/settings/preferences",
json={"user_name": f"Ethan{sep}{PAYLOAD}", "honorific": "sir"},
json={"user_name": f"Tony{sep}{PAYLOAD}", "honorific": "sir"},
headers={"Origin": DASHBOARD_ORIGIN})
assert r.status_code == 400, (hex(ord(sep)), r.text)
_lines, parsed = server._read_env()
Expand All @@ -123,7 +123,7 @@ def test_the_honorific_goes_through_the_same_gate(env, sep):
hand-written list, so it had the same hole."""
c, server = env
r = c.post("/api/settings/preferences",
json={"user_name": "Ethan", "honorific": f"sir{sep}{PAYLOAD}"},
json={"user_name": "Tony", "honorific": f"sir{sep}{PAYLOAD}"},
headers={"Origin": DASHBOARD_ORIGIN})
assert r.status_code == 400, (hex(ord(sep)), r.text)
assert "JARVIS_CLAUDE_PATH" not in server._read_env()[1]
Expand All @@ -132,7 +132,7 @@ def test_the_honorific_goes_through_the_same_gate(env, sep):
def test_the_crlf_pair_is_refused(env):
_c, server = env
with pytest.raises(ValueError):
server._write_env_key("USER_NAME", f"Ethan\r\n{PAYLOAD}")
server._write_env_key("USER_NAME", f"Tony\r\n{PAYLOAD}")


def test_a_null_byte_is_still_refused(env):
Expand All @@ -141,7 +141,7 @@ def test_a_null_byte_is_still_refused(env):
path to a C API, so it keeps its own rule and its own reason."""
_c, server = env
with pytest.raises(ValueError):
server._write_env_key("USER_NAME", "Ethan\0evil")
server._write_env_key("USER_NAME", "Tony\0evil")


def test_the_writer_refuses_everything_the_reader_splits_on(env):
Expand Down Expand Up @@ -200,7 +200,7 @@ def test_every_ordinary_printable_character_still_saves(env):
def test_ordinary_values_still_save(env):
"""A gate that refuses everything is not a gate, it is an outage."""
_c, server = env
for value in ("Ethan Rogers", "sir", "O'Brien", "Ethan-Rogers_1",
for value in ("Tony Stark", "sir", "O'Brien", "Tony-Stark_1",
"Éthan Røgers", "🫡", "sk-live-abc123.def_456",
"a=b=c", "#notacomment", ""):
server._write_env_key("USER_NAME", value)
Expand All @@ -209,11 +209,11 @@ def test_ordinary_values_still_save(env):

def test_a_value_the_reader_would_change_is_refused(env):
"""Deliberate, and the reason is honesty rather than injection: the
reader strips whitespace and one layer of quotes, so ` Ethan ` would
come back as `Ethan` and `'Ethan'` as `Ethan`. Saying "saved" and
reader strips whitespace and one layer of quotes, so ` Tony ` would
come back as `Tony` and `'Tony'` as `Tony`. Saying "saved" and
storing something else is the same class of lie as reporting a stalled
run as a success."""
_c, server = env
for value in (" Ethan ", "'Ethan'", '"Ethan"', "Ethan\t"):
for value in (" Tony ", "'Tony'", '"Tony"', "Tony\t"):
with pytest.raises(ValueError):
server._write_env_key("USER_NAME", value)
30 changes: 15 additions & 15 deletions tests/test_jarvis_memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ def test_the_layout_is_created_once_and_is_idempotent(home):


def test_a_memory_is_one_file_with_a_readable_name(home):
path = jm.write_memory("Ethan prefers Postgres over SQLite",
path = jm.write_memory("Tony prefers Postgres over SQLite",
"He said so while we were fixing chitauri.")

assert path.parent == home / "memory"
assert path.name == "ethan-prefers-postgres-over-sqlite.md"
assert path.name == "tony-prefers-postgres-over-sqlite.md"
text = path.read_text()
assert "Ethan prefers Postgres over SQLite" in text
assert "Tony prefers Postgres over SQLite" in text
assert "chitauri" in text


Expand All @@ -41,7 +41,7 @@ def test_writing_the_same_title_twice_updates_rather_than_duplicating(home):


def test_slugify_makes_a_filename_out_of_anything_sayable(home):
assert jm.slugify("Ethan's 'chitauri' work — Q3/2026!") == "ethans-chitauri-work-q3-2026"
assert jm.slugify("Tony's 'chitauri' work — Q3/2026!") == "tonys-chitauri-work-q3-2026"
assert jm.slugify(" ") == "note"
assert len(jm.slugify("x" * 200)) <= 60

Expand All @@ -51,13 +51,13 @@ def test_reading_a_memory_that_does_not_exist_returns_none(home):


def test_the_index_gains_one_line_per_memory(home):
jm.add_to_index("Ethan prefers Postgres", "said during chitauri work")
jm.add_to_index("Tony prefers Postgres", "said during chitauri work")
jm.add_to_index("Hammer is the worker project", "not to be confused with hammer-private")

lines = jm.index_lines()
assert len(lines) == 2
assert all(line.startswith("- [") for line in lines)
assert "ethan-prefers-postgres.md" in lines[0]
assert "tony-prefers-postgres.md" in lines[0]


def test_the_index_does_not_duplicate_a_title(home):
Expand Down Expand Up @@ -95,7 +95,7 @@ def test_reading_an_unknown_project_note_returns_none(home):


def test_a_journal_entry_is_timestamped_and_labelled(home):
p = jm.write_journal("Worked on chitauri. Ethan wants Postgres.", reason="rotation")
p = jm.write_journal("Worked on chitauri. Tony wants Postgres.", reason="rotation")

assert p.parent == home / "journal"
assert p.name.endswith("-rotation.md")
Expand All @@ -121,14 +121,14 @@ def test_add_to_index_preserves_hand_written_prose(home):
path = home / "MEMORY.md"
path.write_text(
"# What JARVIS remembers\n\n"
"Ethan asked me to always check the chitauri staging env before prod.\n\n"
"Tony asked me to always check the chitauri staging env before prod.\n\n"
"- [Old fact](old-fact.md) — some hook\n"
)

jm.add_to_index("New fact", "a hook")

text = path.read_text()
assert "Ethan asked me to always check the chitauri staging env before prod." in text
assert "Tony asked me to always check the chitauri staging env before prod." in text
assert "- [Old fact](old-fact.md) — some hook" in text
assert "- [New fact](new-fact.md) — a hook" in text

Expand Down Expand Up @@ -163,12 +163,12 @@ def test_latest_journal_is_correct_even_when_reasons_sort_out_of_order(home):


def test_search_finds_a_memory_by_its_words(home):
jm.write_memory("Ethan prefers Postgres", "He said so during chitauri work.")
jm.write_memory("Tony prefers Postgres", "He said so during chitauri work.")
jm.write_memory("Hammer is the worker project", "Runs on the Desktop.")

hits = jm.search("postgres")

assert hits and hits[0]["name"] == "ethan-prefers-postgres"
assert hits and hits[0]["name"] == "tony-prefers-postgres"
assert hits[0]["kind"] == "memory"
assert "Postgres" in hits[0]["excerpt"]

Expand Down Expand Up @@ -282,8 +282,8 @@ def test_two_genuinely_different_titles_that_slugify_identically_both_survive(ho


def test_the_same_title_written_twice_still_produces_one_file(home):
p1 = jm.write_memory("Ethan's DB choice", "uses postgres")
p2 = jm.write_memory("Ethan's DB choice", "uses sqlite")
p1 = jm.write_memory("Tony's DB choice", "uses postgres")
p2 = jm.write_memory("Tony's DB choice", "uses sqlite")

assert p1 == p2
assert len(jm.list_memories()) == 1
Expand All @@ -293,8 +293,8 @@ def test_the_same_title_written_twice_still_produces_one_file(home):


def test_a_title_differing_only_by_apostrophe_or_case_is_the_same_memory(home):
p1 = jm.write_memory("Ethan's DB choice", "uses postgres")
p2 = jm.write_memory("ethans db choice", "uses sqlite")
p1 = jm.write_memory("Tony's DB choice", "uses postgres")
p2 = jm.write_memory("tonys db choice", "uses sqlite")

assert p1 == p2
assert len(jm.list_memories()) == 1
Expand Down
12 changes: 6 additions & 6 deletions tests/test_memory_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,19 @@ def test_a_read_never_creates_the_folder(api):

def test_the_snapshot_lists_memories_projects_and_the_journal(api):
client, jm, _dp, _rs = api
jm.write_memory("Ethan prefers Postgres over SQLite", "for chitauri")
jm.add_to_index("Ethan prefers Postgres over SQLite", "database preference")
jm.write_memory("Tony prefers Postgres over SQLite", "for chitauri")
jm.add_to_index("Tony prefers Postgres over SQLite", "database preference")
jm.write_project_note("chitauri", "The billing job runs nightly.")
jm.write_journal("We shipped the dashboard.", reason="shutdown")

body = client.get("/api/memory").json()

assert body["index"] == [{"title": "Ethan prefers Postgres over SQLite",
"slug": "ethan-prefers-postgres-over-sqlite",
assert body["index"] == [{"title": "Tony prefers Postgres over SQLite",
"slug": "tony-prefers-postgres-over-sqlite",
"hook": "database preference"}]
assert [m["slug"] for m in body["memories"]] == \
["ethan-prefers-postgres-over-sqlite"]
assert body["memories"][0]["title"] == "Ethan prefers Postgres over SQLite"
["tony-prefers-postgres-over-sqlite"]
assert body["memories"][0]["title"] == "Tony prefers Postgres over SQLite"
assert isinstance(body["memories"][0]["modified"], (int, float))
assert [p["slug"] for p in body["projects"]] == ["chitauri"]
assert len(body["journal"]) == 1
Expand Down
12 changes: 6 additions & 6 deletions tests/test_memory_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,19 @@ class _WatcherTurn:

def test_remember_writes_a_file_and_indexes_it(wired):
server = wired
out = server.tool_remember({"title": "Ethan prefers Postgres",
out = server.tool_remember({"title": "Tony prefers Postgres",
"body": "Said during chitauri work.",
"hook": "database preference"})

import jarvis_memory as jm
assert "ethan-prefers-postgres" in jm.list_memories()
assert "tony-prefers-postgres" in jm.list_memories()
assert any("database preference" in ln for ln in jm.index_lines())
assert "remember" in out.lower() or "noted" in out.lower()


def test_recall_finds_what_was_remembered(wired):
server = wired
server.tool_remember({"title": "Ethan prefers Postgres", "body": "for chitauri",
server.tool_remember({"title": "Tony prefers Postgres", "body": "for chitauri",
"hook": "db"})

out = server.tool_recall({"query": "postgres"})
Expand All @@ -45,16 +45,16 @@ def test_recall_says_so_plainly_when_it_finds_nothing(wired):


def test_recall_never_speaks_a_memorys_raw_slug(wired):
"""A memory's filename is a slugified title ("ethan-prefers-postgres");
"""A memory's filename is a slugified title ("tony-prefers-postgres");
read aloud that's noise, not a sentence — see tool_list_sessions's own
rule against ever saying a roster name like hammer-4b out loud."""
server = wired
server.tool_remember({"title": "Ethan prefers Postgres over SQLite",
server.tool_remember({"title": "Tony prefers Postgres over SQLite",
"body": "for chitauri", "hook": "db"})

out = server.tool_recall({"query": "postgres"})

assert "ethan-prefers-postgres" not in out.lower()
assert "tony-prefers-postgres" not in out.lower()


def test_recall_never_speaks_a_journal_entrys_raw_timestamp(wired):
Expand Down
10 changes: 5 additions & 5 deletions tests/test_memory_writers.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,14 +210,14 @@ def test_the_index_stays_parseable_after_a_hostile_title(memory):


def test_an_ordinary_memory_still_reads_as_itself(memory):
memory.write_memory("Ethan prefers postgres", "He said so on Tuesday.")
memory.add_to_index("Ethan prefers postgres", "over sqlite, every time")
memory.write_memory("Tony prefers postgres", "He said so on Tuesday.")
memory.add_to_index("Tony prefers postgres", "over sqlite, every time")
rows = memory.index_entries()
assert rows == [{"title": "Ethan prefers postgres",
"slug": "ethan-prefers-postgres",
assert rows == [{"title": "Tony prefers postgres",
"slug": "tony-prefers-postgres",
"hook": "over sqlite, every time"}], rows
assert "He said so on Tuesday." in memory.read_memory(
"Ethan prefers postgres")
"Tony prefers postgres")


def test_a_multi_line_body_is_still_allowed(memory):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_rotation_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def __init__(self, pending=True, overdue=False, rotates=True):
async def turn(self, text, origin="user", on_delta=None):
self.asked.append((text, origin))
import brain
return brain.TurnResult(origin, "We fixed chitauri and Ethan chose Postgres.",
return brain.TurnResult(origin, "We fixed chitauri and Tony chose Postgres.",
"result")

async def rotate(self, handover=None):
Expand Down
2 changes: 1 addition & 1 deletion tests/test_untrusted_turn.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ def test_a_later_clean_turn_may_still_write(call):
assert _call("remember", title="X", body="Y")["ok"] is False

brain.new_turn()
out = _call("remember", title="Ethan prefers Postgres",
out = _call("remember", title="Tony prefers Postgres",
body="He said so out loud.")
assert out["ok"] is True, out
assert "Postgres" in jarvis_memory._index_path().read_text()
Expand Down
Loading
Loading