agentltl-coding 0.2.4__py3-none-any.whl

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.
Files changed (40) hide show
  1. agentltl_coding/__init__.py +21 -0
  2. agentltl_coding/cli.py +482 -0
  3. agentltl_coding/edit.py +67 -0
  4. agentltl_coding/guard.py +461 -0
  5. agentltl_coding/harness.py +110 -0
  6. agentltl_coding/library/ask-before-creating-credentials.yaml +9 -0
  7. agentltl_coding/library/ask-before-discarding-work.yaml +12 -0
  8. agentltl_coding/library/ask-before-infra-changes.yaml +9 -0
  9. agentltl_coding/library/ask-before-installing.yaml +12 -0
  10. agentltl_coding/library/ask-before-reading-secret-stores.yaml +35 -0
  11. agentltl_coding/library/ask-before-recursive-delete.yaml +7 -0
  12. agentltl_coding/library/devops-secrets.yaml +10 -0
  13. agentltl_coding/library/no-claude-coauthor.yaml +11 -0
  14. agentltl_coding/library/no-codex-coauthor.yaml +11 -0
  15. agentltl_coding/library/no-copilot-coauthor.yaml +11 -0
  16. agentltl_coding/library/no-env-dumps.yaml +18 -0
  17. agentltl_coding/library/no-force-push.yaml +7 -0
  18. agentltl_coding/library/no-leaky-debug.yaml +13 -0
  19. agentltl_coding/library/no-push-to-main.yaml +7 -0
  20. agentltl_coding/library/no-secrets-in-commands.yaml +21 -0
  21. agentltl_coding/library/no-skipping-secret-scans.yaml +9 -0
  22. agentltl_coding/library/no-vibe-coauthor.yaml +11 -0
  23. agentltl_coding/library/protect-env-files.yaml +9 -0
  24. agentltl_coding/library/protect-secret-files.yaml +53 -0
  25. agentltl_coding/library/read-before-overwrite.yaml +11 -0
  26. agentltl_coding/library/subagents-on-sonnet.yaml +12 -0
  27. agentltl_coding/library/tests-before-finishing.yaml +13 -0
  28. agentltl_coding/library/tests-before-push.yaml +14 -0
  29. agentltl_coding/memory.py +390 -0
  30. agentltl_coding/pattern.py +291 -0
  31. agentltl_coding/rules.py +920 -0
  32. agentltl_coding/scan.py +54 -0
  33. agentltl_coding/session.py +149 -0
  34. agentltl_coding/store.py +123 -0
  35. agentltl_coding-0.2.4.dist-info/METADATA +129 -0
  36. agentltl_coding-0.2.4.dist-info/RECORD +40 -0
  37. agentltl_coding-0.2.4.dist-info/WHEEL +5 -0
  38. agentltl_coding-0.2.4.dist-info/entry_points.txt +2 -0
  39. agentltl_coding-0.2.4.dist-info/licenses/LICENSE +21 -0
  40. agentltl_coding-0.2.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,54 @@
1
+ """
2
+ agentltl_coding/scan.py – spot credentials in what a tool call returned.
3
+
4
+ A rule can only stop a call before it runs; when a program prints a credential anyway
5
+ (`python app.py` logging its config, a test failure dumping the environment), the value is
6
+ already in the conversation. The harness's after-the-call hook scans the output for well-known credential
7
+ formats, the documented prefixes that secret scanners also use, and reports what kind was
8
+ seen, never the value, so the user can rotate it and the agent does not repeat it.
9
+
10
+ Only formats with a distinctive prefix or structure are matched, to keep false alarms rare.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import re
17
+ from typing import Any, List, Tuple
18
+
19
+ _MAX_SCAN = 1_000_000
20
+
21
+ # (kind, pattern): documented prefixes; lengths keep ordinary words from matching.
22
+ _FORMATS = [
23
+ ("AWS access key ID", r"\b(?:AKIA|ASIA)[0-9A-Z]{16}\b"),
24
+ ("GitHub token", r"\b(?:gh[pousr]_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{60,})\b"),
25
+ ("GitLab token", r"\bglpat-[A-Za-z0-9_-]{20,}\b"),
26
+ ("Slack token", r"\bxox[abprs]-[A-Za-z0-9-]{10,}\b"),
27
+ ("Stripe live key", r"\b[rs]k_live_[A-Za-z0-9]{20,}\b"),
28
+ ("Google API key", r"\bAIza[0-9A-Za-z_-]{35}\b"),
29
+ ("Anthropic API key", r"\bsk-ant-[A-Za-z0-9_-]{20,}"),
30
+ ("OpenAI API key", r"\bsk-(?:proj-)?[A-Za-z0-9_-]{40,}"),
31
+ ("npm token", r"\bnpm_[A-Za-z0-9]{36}\b"),
32
+ ("private key", r"-----BEGIN (?:[A-Z]+ )?PRIVATE KEY-----"),
33
+ ]
34
+ _COMPILED = [(kind, re.compile(pattern)) for kind, pattern in _FORMATS]
35
+
36
+
37
+ def credential_kinds(value: Any) -> List[str]:
38
+ """The kinds of credential that appear in *value* (a tool response), in a stable order."""
39
+ text = value if isinstance(value, str) else json.dumps(value, default=str)
40
+ text = text[:_MAX_SCAN]
41
+ return [kind for kind, pattern in _COMPILED if pattern.search(text)]
42
+
43
+
44
+ def report(tool: str, kinds: List[str]) -> Tuple[str, str]:
45
+ """What to say after a call whose output has credentials of *kinds*: a notice for the
46
+ user, and an instruction for the agent. The harness puts each where its agent shows it."""
47
+ what = ", ".join(kinds)
48
+ user = (f"AgentLTL: the output of {tool} contains what looks like a credential ({what}). "
49
+ "If it is real, consider rotating it.")
50
+ agent = (f"The output of this {tool} call contains what looks like a credential ({what}). "
51
+ "Do not repeat, quote, copy or write that value anywhere: not in replies, files, "
52
+ "commands or commit messages. Tell the user it appeared, so they can rotate it, "
53
+ "and carry on without it.")
54
+ return user, agent
@@ -0,0 +1,149 @@
1
+ """
2
+ agentltl_coding/session.py – what a harness's hooks do, whatever their I/O.
3
+
4
+ Each agent calls its hooks with its own payloads and expects its own JSON back; what they
5
+ do in between is the same everywhere. A harness turns its payload into one of these calls
6
+ and the result into its answer::
7
+
8
+ s = Session(cwd, project, session_id)
9
+ if not s.files: ... # no AGENTLTL.yaml: stay out of the way
10
+ s.prompt() # the user wrote: lift a stop, new turn
11
+ s.start() # the reminder of the rules, or None
12
+ s.pre("Bash", {"command": "git push"}, auto=False) # a Verdict
13
+ s.post("Bash", {...}, "id", output, status=0) # (user notice, agent note) or None
14
+ s.finish() # a Verdict: "block" while `finally` is unmet
15
+
16
+ Tool names may be the agent's own: the guard maps them (``Harness.tool_aliases``).
17
+ :class:`~agentltl_coding.rules.RuleFileError` from a broken rule file reaches the caller,
18
+ which tells the user (see :func:`broken`).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import logging
24
+ from typing import Any, Dict, List, Optional, Tuple
25
+
26
+ from . import store
27
+ from .guard import Guard, Verdict
28
+ from .harness import current as _harness
29
+ from .pattern import Paths
30
+ from .rules import RuleSet, load, rule_files
31
+
32
+ STILL_STOPPED = (
33
+ "[AGENTLTL] Rule '{rule}' stopped this session: every tool call is refused until the user "
34
+ "replies. Nothing was executed. Don't try another command or a workaround. Tell the user "
35
+ "what you were trying to do, why, and what you need from them, then end your turn.")
36
+ _MAX_DECISIONS = 200
37
+
38
+
39
+ class Session:
40
+ def __init__(self, cwd: str, project: str, session_id: Optional[str]) -> None:
41
+ self.cwd, self.project, self.sid = cwd, project, session_id or "default"
42
+ self.files: List[str] = rule_files(cwd, project)
43
+ self._ruleset: Optional[RuleSet] = None
44
+ logging.getLogger("agentltl").setLevel(logging.ERROR)
45
+ logging.getLogger("cli_to_tools").setLevel(logging.ERROR)
46
+
47
+ @property
48
+ def ruleset(self) -> RuleSet:
49
+ """The compiled rules. Raises RuleFileError for a broken rule file."""
50
+ if self._ruleset is None:
51
+ self._ruleset = load(self.files, Paths(self.cwd, self.project))
52
+ return self._ruleset
53
+
54
+ # ── events ────────────────────────────────────────────────────────────────
55
+
56
+ def prompt(self) -> None:
57
+ """The user wrote: lift a stop (even if the rule file has since become unreadable),
58
+ and let ``finally`` rules send the agent back again in this new turn."""
59
+ with store.locked(self.sid) as state:
60
+ state.pop("stopped", None)
61
+ state["termination_nudges"] = 0
62
+ with store.locked_project(self.project) as project_state:
63
+ project_state["termination_nudges"] = 0
64
+
65
+ def start(self) -> Optional[str]:
66
+ """What to tell the agent at the start of a session (None: settings.announce off)."""
67
+ return reminder(self.ruleset)
68
+
69
+ def pre(self, tool: str, tool_input: Dict[str, Any], *, auto: bool = False) -> Verdict:
70
+ """Decide a call before it runs. After a ``stop``, every call is denied until
71
+ :meth:`prompt`."""
72
+ def decide(guard: Guard, state: Dict[str, Any]) -> Verdict:
73
+ if state.get("stopped"):
74
+ return Verdict("deny", STILL_STOPPED.format(**state["stopped"]),
75
+ rule=state["stopped"].get("rule"))
76
+ verdict = guard.decide(tool, tool_input, auto=auto)
77
+ if verdict.action == "stop":
78
+ state["stopped"] = {"rule": verdict.rule or "?"}
79
+ return verdict
80
+
81
+ return self._with_guard(decide, _harness().canonical(tool, tool_input))
82
+
83
+ def post(self, tool: str, tool_input: Dict[str, Any], tool_id: str, result: Any,
84
+ status: Optional[int] = None) -> Optional[Tuple[str, str]]:
85
+ """Record a call that ran (status 0) or failed (1). Returns what to say when its output
86
+ holds a credential: (notice for the user, instruction for the agent)."""
87
+ self._with_guard(lambda guard, state: guard.record(tool, tool_input, tool_id, result,
88
+ status=status))
89
+ if not self.ruleset.settings.scan_output or result is None:
90
+ return None
91
+ from .scan import credential_kinds, report
92
+ kinds = credential_kinds(result)
93
+ return report(_harness().canonical(tool, {})[0], kinds) if kinds else None
94
+
95
+ def finish(self) -> Verdict:
96
+ """The agent is about to end its turn: "block" with what is missing while a
97
+ ``finally`` rule is unmet."""
98
+ return self._with_guard(lambda guard, state: guard.finish()) or Verdict()
99
+
100
+ # ── state ─────────────────────────────────────────────────────────────────
101
+
102
+ def _with_guard(self, fn: Any, call: Optional[Tuple[str, Dict[str, Any]]] = None) -> Any:
103
+ """Run *fn(guard, session_state)* on the saved state and save it again. A verdict on
104
+ *call* that intervenes is logged for ``agentltl trace``."""
105
+ guard = Guard(self.ruleset, Paths(self.cwd, self.project))
106
+ with store.locked(self.sid) as state, store.locked_project(self.project) as project_state:
107
+ guard.restore(state, project_state)
108
+ out = fn(guard, state)
109
+ for old in ("trace", "engine"): # saved before AgentLTL 0.2
110
+ state.pop(old, None)
111
+ state.update(guard.dump())
112
+ state["project_dir"] = self.project
113
+ project_state.update(guard.dump_project())
114
+ if call and isinstance(out, Verdict) and out.action not in ("none", "block"):
115
+ state.setdefault("decisions", []).append(
116
+ {"tool": call[0], "input": call[1], "action": out.action, "rule": out.rule})
117
+ state["decisions"] = state["decisions"][-_MAX_DECISIONS:]
118
+ return out
119
+
120
+
121
+ def reminder(ruleset: RuleSet) -> Optional[str]:
122
+ """The session-start reminder: the rules in force, one line each."""
123
+ if not ruleset.settings.announce:
124
+ return None
125
+ lines = [
126
+ f"This project enforces {len(ruleset.rules)} AGENTLTL rule(s) on every tool call, shell "
127
+ "commands included (each command line is checked as the sequence of commands it runs). "
128
+ "A call that breaks a rule is refused with the reason; follow it rather than working "
129
+ "around it. When a rule keeps you from doing what the user asked, or refuses a call, "
130
+ "tell the user in your reply and name the rule. Rules:",
131
+ ]
132
+ note = _harness().memory_note
133
+ for r in ruleset.rules:
134
+ if r.id == "memory-first" and r.kind == "never" and note:
135
+ lines.append(f"- memory-first [{r.mode}]: {note}")
136
+ continue
137
+ why = f" — {r.why}" if r.why else ""
138
+ memory = ", whole project" if r.scope == "project" else ""
139
+ lines.append(f"- {r.id} [{r.mode}{memory}]: {r.summary}{why}")
140
+ return "\n".join(lines)
141
+
142
+
143
+ def broken(problems: List[str]) -> str:
144
+ """What to tell the agent (and through it the user) about a broken rule file."""
145
+ return ("AGENTLTL.yaml has errors, so NO AGENTLTL rules are being enforced until it is "
146
+ "fixed:\n" + "\n".join(f"- {p}" for p in problems))
147
+
148
+
149
+ __all__ = ["Session", "STILL_STOPPED", "broken", "reminder"]
@@ -0,0 +1,123 @@
1
+ """
2
+ agentltl_coding/store.py – per-session state on disk.
3
+
4
+ Each hook call is a fresh process, so traces and enforcer counters live on disk: the
5
+ session memory in ``<state dir>/<session id>.json``, the project memory (every call made in
6
+ a project, across sessions) in ``<state dir>/../projects/<hash of the project dir>.json``. A harness may run tool calls in parallel, so every
7
+ read-modify-write holds an exclusive lock on ``<file>.lock``.
8
+
9
+ State dir: ``$AGENTLTL_STATE`` (or ``$AGENTLTL_CC_STATE``), else the harness's ``state_dir``,
10
+ else ``sessions/`` next to the virtualenv the guard runs in (``<data>/venv-<pins>`` →
11
+ ``<data>/sessions``, so a hook and the ``agentltl`` CLI agree without sharing an
12
+ environment), else ``~/.cache/agentltl-<harness>/sessions``.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import fcntl
18
+ import hashlib
19
+ import json
20
+ import os
21
+ import re
22
+ import sys
23
+ import tempfile
24
+ from contextlib import contextmanager
25
+ from typing import Any, Dict, Iterator, List, Optional
26
+
27
+
28
+ def state_dir() -> str:
29
+ from .harness import current
30
+ for var in ("AGENTLTL_STATE", "AGENTLTL_CC_STATE"):
31
+ if os.environ.get(var):
32
+ return os.environ[var]
33
+ if current().state_dir:
34
+ return os.path.expanduser(current().state_dir)
35
+ if re.fullmatch(r"venv(-\d+)?", os.path.basename(sys.prefix)):
36
+ return os.path.join(os.path.dirname(sys.prefix), "sessions")
37
+ return os.path.join(os.path.expanduser("~"), ".cache", f"agentltl-{current().name}", "sessions")
38
+
39
+
40
+ def session_path(session_id: str) -> str:
41
+ safe = re.sub(r"[^A-Za-z0-9_.-]", "_", session_id or "default")[:128]
42
+ return os.path.join(state_dir(), f"{safe}.json")
43
+
44
+
45
+ def project_path(project_dir: str) -> str:
46
+ key = hashlib.sha1(os.path.abspath(project_dir).encode()).hexdigest()[:16]
47
+ return os.path.join(os.path.dirname(state_dir()), "projects", f"{key}.json")
48
+
49
+
50
+ def _read(path: str) -> Dict[str, Any]:
51
+ try:
52
+ with open(path, encoding="utf-8") as fh:
53
+ return json.load(fh)
54
+ except (OSError, ValueError):
55
+ return {}
56
+
57
+
58
+ def read(session_id: str) -> Dict[str, Any]:
59
+ return _read(session_path(session_id))
60
+
61
+
62
+ def read_project(project_dir: str) -> Dict[str, Any]:
63
+ return _read(project_path(project_dir))
64
+
65
+
66
+ @contextmanager
67
+ def _locked(path: str) -> Iterator[Dict[str, Any]]:
68
+ os.makedirs(os.path.dirname(path), exist_ok=True)
69
+ with open(path + ".lock", "w") as lock:
70
+ fcntl.flock(lock, fcntl.LOCK_EX)
71
+ state = _read(path)
72
+ yield state
73
+ fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path), suffix=".tmp")
74
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
75
+ json.dump(state, fh, default=str)
76
+ os.replace(tmp, path)
77
+
78
+
79
+ @contextmanager
80
+ def locked(session_id: str) -> Iterator[Dict[str, Any]]:
81
+ """Yield the session state for update; whatever it holds on exit is written back."""
82
+ with _locked(session_path(session_id)) as state:
83
+ yield state
84
+
85
+
86
+ @contextmanager
87
+ def locked_project(project_dir: str) -> Iterator[Dict[str, Any]]:
88
+ """Like :func:`locked`, for the project memory. Take it after the session lock."""
89
+ with _locked(project_path(project_dir)) as state:
90
+ state["project_dir"] = os.path.abspath(project_dir)
91
+ yield state
92
+
93
+
94
+ def latest_session(cwd: Optional[str] = None) -> Optional[str]:
95
+ """The most recently updated session, optionally only one whose project contains *cwd*."""
96
+ folder = state_dir()
97
+ try:
98
+ names = [n for n in os.listdir(folder) if n.endswith(".json")]
99
+ except OSError:
100
+ return None
101
+ names.sort(key=lambda n: os.path.getmtime(os.path.join(folder, n)), reverse=True)
102
+ for name in names:
103
+ sid = name[:-5]
104
+ project = read(sid).get("project_dir")
105
+ if cwd is None or (project and (cwd == project or cwd.startswith(project + os.sep))):
106
+ return sid
107
+ return None
108
+
109
+
110
+ def reset(session_id: str) -> None:
111
+ with locked(session_id) as state:
112
+ keep = {k: state[k] for k in ("project_dir",) if k in state}
113
+ state.clear()
114
+ state.update(keep)
115
+
116
+
117
+ def reset_project(project_dir: str) -> None:
118
+ with locked_project(project_dir) as state:
119
+ state.clear()
120
+
121
+
122
+ __all__: List[str] = ["state_dir", "session_path", "project_path", "read", "read_project",
123
+ "locked", "locked_project", "latest_session", "reset", "reset_project"]
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: agentltl-coding
3
+ Version: 0.2.4
4
+ Summary: AgentLTL rules for coding agents: the AGENTLTL.yaml language, a rule library, and a guard any harness can call.
5
+ License: MIT
6
+ Project-URL: Homepage, https://agentltl.github.io
7
+ Project-URL: Source, https://github.com/AgentLTL/agentltl-coding
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: agentltl>=0.2
12
+ Requires-Dist: cli-to-tools>=0.2
13
+ Requires-Dist: pyyaml>=6.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest>=7.0; extra == "dev"
16
+ Requires-Dist: ruff>=0.4; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # agentltl-coding
20
+
21
+ AgentLTL rules for coding agents, whatever the harness: the `AGENTLTL.yaml` rule language, a
22
+ library of tested rules, and a guard that decides each tool call before it runs.
23
+
24
+ - **Rules** in plain YAML, compiled to [AgentLTL](https://github.com/AgentLTL/AgentLTL)
25
+ formulas over the calls made so far:
26
+
27
+ ```yaml
28
+ rules:
29
+ - id: tests-before-push
30
+ before: {first: pytest, then: git_push, since: [Edit, Write]}
31
+ why: CI is slow; run the tests locally first.
32
+ mode: warn
33
+ use: [no-force-push, protect-secret-files] # from the library
34
+ ```
35
+
36
+ - **Shell commands** are checked as the structured calls of their command line
37
+ ([cli-to-tools](https://github.com/AgentLTL/cli-to-tools)): `git add . && git push -f` is
38
+ `git_add`, then `git_push {force: true}`, checked all or nothing, with redirections,
39
+ paths after `cd`, and files only known at run time accounted for.
40
+ - **Modes** map one to one onto AgentLTL severities: `block`, `warn` (the agent may insist),
41
+ `ask` (you decide), `retry` (then you decide), `stop`, `log`.
42
+ - **Memory:** each rule reads the calls of this session, or of the project across sessions.
43
+
44
+ Four harnesses are built on it: the
45
+ [Claude Code plugin](https://github.com/AgentLTL/agentltl-claude-code), the
46
+ [GitHub Copilot CLI plugin](https://github.com/AgentLTL/agentltl-copilot-cli), the
47
+ [Mistral Vibe plugin](https://github.com/AgentLTL/agentltl-mistral-vibe) and the
48
+ [Codex plugin](https://github.com/AgentLTL/agentltl-codex). The full rule
49
+ reference is on [agentltl.github.io](https://agentltl.github.io/rules/).
50
+
51
+ ## Installation
52
+
53
+ Most people want a plugin for their agent, not this library: paste the prompt from the
54
+ plugin's README into your agent ([Claude Code](https://github.com/AgentLTL/agentltl-claude-code#installation),
55
+ [Codex](https://github.com/AgentLTL/agentltl-codex#installation),
56
+ [Copilot CLI](https://github.com/AgentLTL/agentltl-copilot-cli#installation),
57
+ [Mistral Vibe](https://github.com/AgentLTL/agentltl-mistral-vibe#installation)). To build a
58
+ harness for another agent, paste this into your coding agent:
59
+
60
+ ```
61
+ Install agentltl-coding (pip install "git+https://github.com/AgentLTL/agentltl-coding.git" in a virtual environment) and read its README at https://github.com/AgentLTL/agentltl-coding, then help me write a harness for my agent.
62
+ ```
63
+
64
+ ## A harness in a few lines
65
+
66
+ ```python
67
+ from agentltl_coding import Harness, Session, configure
68
+
69
+ configure(Harness(name="my-agent", agent="the agent", user_dir="~/.my-agent",
70
+ shell_tools={"Bash": "command"},
71
+ tool_aliases={"shell": ("Bash", {"cmd": "command"}),
72
+ "write_file": ("Write", {"path": "file_path"})}))
73
+
74
+ s = Session(cwd, project_root, session_id) # one per hook call; state is on disk
75
+ if s.files: # an AGENTLTL.yaml applies here
76
+ verdict = s.pre("shell", {"cmd": "git push --force"}) # before the call
77
+ # verdict.action: "none" | "deny" | "ask" | "stop"; verdict.reason: the message
78
+ ...
79
+ s.post("shell", {"cmd": "pytest"}, call_id, output, status=0) # after it ran
80
+ s.finish() # the agent wants to stop: "block" while a `finally` rule is unmet
81
+ s.prompt() # the user wrote: lift a `stop`
82
+ ```
83
+
84
+ `Harness` says what differs between agents:
85
+
86
+ - the agent's name in messages, and where the user's `AGENTLTL.yaml` lives;
87
+ - `tool_aliases`: its tool names mapped onto the canonical ones rules use (Claude Code's
88
+ `Bash`, `Write`, `Edit`, `Read`...), so the library works unchanged (Copilot's
89
+ `create {path, file_text}` is `Write {file_path, content}`);
90
+ - which tools take shell command lines, and the permission modes in which nobody answers a
91
+ prompt;
92
+ - built-in rules of its own (`memory-first`), and where its memory lives, for
93
+ `agentltl memory scan` (`agentltl_coding.memory`).
94
+
95
+ `Session` is what the hooks do: it keeps the session and project traces on disk, holds a
96
+ `stop` until the user replies, logs interventions, and scans call output for credentials.
97
+ `Guard` underneath decides one call. `agentltl_coding.cli.main(argv, extend=...)` is the
98
+ `agentltl` command (validate, check, translate, tools, library, use, memory...), to which a
99
+ harness can add commands. A library entry with `harnesses: [claude-code]` is for that agent
100
+ only; elsewhere `use:` of it switches nothing on, so one project file serves every agent.
101
+
102
+ ## Testing a harness end to end
103
+
104
+ `e2e/` lets a plugin test itself in the real agent, without a model or an account:
105
+
106
+ - `e2e/scripted_model.py SCRIPT.json` serves the OpenAI chat-completions, OpenAI responses
107
+ and Anthropic messages formats (Mistral Vibe, Copilot CLI with a custom provider, Codex with
108
+ `wire_api = "responses"`, Claude Code with `ANTHROPIC_BASE_URL`) and answers each request with the next step of a script: tool calls,
109
+ then a final text.
110
+ - `e2e/check.py TRACE EXPECTED` checks that `agentltl trace` shows the expected refusals, in
111
+ order.
112
+ - `.github/workflows/agent-e2e.yml` is a reusable workflow: it runs the plugin's
113
+ `docker/e2e.sh` and, when that fails on a pull request (such as Dependabot's update of the
114
+ agent's version), says so on the pull request and mentions who to notify.
115
+
116
+ Each plugin pins the agent version it tests in `docker/` (a `package.json` or a
117
+ `requirements.txt`), so Dependabot proposes every new release as a pull request.
118
+
119
+ ## Develop
120
+
121
+ ```bash
122
+ pip install -e ".[dev]" "agentltl @ git+https://github.com/AgentLTL/AgentLTL.git" \
123
+ "cli-to-tools @ git+https://github.com/AgentLTL/cli-to-tools.git"
124
+ pytest -q
125
+ ```
126
+
127
+ `tests/golden/verdicts.json` records what every packaged rule, example file and formula
128
+ rule decides, step by step; a change that alters a verdict regenerates it
129
+ (`AGENTLTL_GOLDEN_UPDATE=1 pytest tests/test_golden.py`) and shows up as a reviewed diff.
@@ -0,0 +1,40 @@
1
+ agentltl_coding/__init__.py,sha256=-bsuqhM7lxy_x8-tdUwDhoh7RGtU5dUk28TGflB0i9A,991
2
+ agentltl_coding/cli.py,sha256=mtbKqiysDywC-hcBRpUL8VuhLbB1il8XEARPLd_QdwE,20686
3
+ agentltl_coding/edit.py,sha256=Fw_IYv-cn7OMKFbRPiHkMulh9E0g6IX5rP56TfqgajQ,2690
4
+ agentltl_coding/guard.py,sha256=tsLz09sGwaFa-cvdMGAefsu3wAg5eoAnbnz2ODeOHVI,23281
5
+ agentltl_coding/harness.py,sha256=xrI5p9N7k6F18DrK2KTHi9DmrYtUhoQmgiK1OYju-VU,4771
6
+ agentltl_coding/memory.py,sha256=ZtDfbcfsEERLgHjrNUpacmt5Y0RK22vkUmGe171tUHo,17820
7
+ agentltl_coding/pattern.py,sha256=eY8C7YSKQNxP41-ZjXBK8_Y-2enZTn00DPLvekYdifw,13234
8
+ agentltl_coding/rules.py,sha256=OtNCKLbf4nwcNKVBm0esOiZmwCNUOH-fDc2CR9Nfqts,41675
9
+ agentltl_coding/scan.py,sha256=owdbGriXg9zvpQdGflO3fL25E9xyIPQeL0eLq6PMz6g,2631
10
+ agentltl_coding/session.py,sha256=i2Kxl5Td7bE1E1B2UZ-0qh097wio0U-Ghbx8tGHOzJ8,7669
11
+ agentltl_coding/store.py,sha256=oDe7HwkUY3QaGU1iEvu_Zu4qVDG_5kpwREfMXC-G-xU,4367
12
+ agentltl_coding/library/ask-before-creating-credentials.yaml,sha256=Al2_JHoo-3JXORY0t8xGz5PtYPInzKaBw8-0YE7-Hag,582
13
+ agentltl_coding/library/ask-before-discarding-work.yaml,sha256=-tn4y_14UyyKb4AD3_hMp7RW_pzzi3YCTc-3KEAxSAI,492
14
+ agentltl_coding/library/ask-before-infra-changes.yaml,sha256=bKGPnrVtdnw6fhufBWw6O1o1vDwaG1LbHvhg_XN9HtE,484
15
+ agentltl_coding/library/ask-before-installing.yaml,sha256=_nHRBBy5jHqgpuz0MdzH5n3OAkDn_KBYtN1rU28HYtk,557
16
+ agentltl_coding/library/ask-before-reading-secret-stores.yaml,sha256=qPgc_V4sOYo9WJ-p6_MBoW190k3IJEcfpvH3D4btZu0,2365
17
+ agentltl_coding/library/ask-before-recursive-delete.yaml,sha256=r9mE6eMg6S-cRJMfKRa0fYa3kIZPCCaAIUwWP-EIu9M,222
18
+ agentltl_coding/library/devops-secrets.yaml,sha256=L86wDUFsM__Hd4auZgHa2Vkx_KH27cp6gvaQdIYY1ME,404
19
+ agentltl_coding/library/no-claude-coauthor.yaml,sha256=XoUgeHt0rt3ueNkO7qDVLkWc5dGJNVxY6RqD2oBMa50,602
20
+ agentltl_coding/library/no-codex-coauthor.yaml,sha256=VJ_-FZMUE2LYNcMQ9Pn2_nw_ric0Mz2QQOZsRX1cCkY,587
21
+ agentltl_coding/library/no-copilot-coauthor.yaml,sha256=Y2zJ_SWrD4kjgr_0pvy2WecbVgusrXb_CO1ZvY54_8A,580
22
+ agentltl_coding/library/no-env-dumps.yaml,sha256=GBdc15SXtBUbgpyAVt6WDR6Cico1vaWImOyyJ-NMTZY,938
23
+ agentltl_coding/library/no-force-push.yaml,sha256=-esr5Ue9gMlZuapyCBpGqV8S0FBKVcTXtyM75JSD9CI,321
24
+ agentltl_coding/library/no-leaky-debug.yaml,sha256=sCUKpPZYvuIWSDVcekf1f3Ht4PtYgglo0KHiHDCLqls,709
25
+ agentltl_coding/library/no-push-to-main.yaml,sha256=VyMt5E9KBjW8WNvi1_UwbDsT6BKgwIiwtzSwDsCMB1U,339
26
+ agentltl_coding/library/no-secrets-in-commands.yaml,sha256=_M_2tAoivUYjBA1nbYZ6jSbK8uiWHMPpgT3Gw65KKFI,1356
27
+ agentltl_coding/library/no-skipping-secret-scans.yaml,sha256=80s2v4BYBWTzNLo1CyCPTAqdSVMUgTcK9bgfa53pCBY,415
28
+ agentltl_coding/library/no-vibe-coauthor.yaml,sha256=ql8uXkQrbQySCFEct3AGBKDFtQ-GLm02eofJXSa5O_Q,620
29
+ agentltl_coding/library/protect-env-files.yaml,sha256=dE91Jc3GWGf6DBaE7qd31-c2o0z7x7wAtoVMpI5G2lI,474
30
+ agentltl_coding/library/protect-secret-files.yaml,sha256=r1lP5rfrLlPyatKm0Kror48bjONqkBHEBN8XcwOSrPk,1660
31
+ agentltl_coding/library/read-before-overwrite.yaml,sha256=VfeVl8hGSvBEv6Ai88bIuA9pW0Txe4GRvBNu75AgsJc,519
32
+ agentltl_coding/library/subagents-on-sonnet.yaml,sha256=arF7txB3cejhpK97GnQ06iy8X3Sz8IHmZTVbpyADtdM,619
33
+ agentltl_coding/library/tests-before-finishing.yaml,sha256=tbRgXDGKDEAutE6w7S8MJe9GtYp4FuvQOC3s7cp-cdI,631
34
+ agentltl_coding/library/tests-before-push.yaml,sha256=-KvlpX_pNOQ-k8mbs8dNO_LusmKSFm42nCrFlNHbXJY,771
35
+ agentltl_coding-0.2.4.dist-info/licenses/LICENSE,sha256=s8Kl1HVH8xqtOmxdNgTs-k9B8E6jYtpoF-u8k5WECac,1072
36
+ agentltl_coding-0.2.4.dist-info/METADATA,sha256=qQzlIa5SqrOPkpqKCNPdYL2482P4_xhnmZejdTS9mAw,6441
37
+ agentltl_coding-0.2.4.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
38
+ agentltl_coding-0.2.4.dist-info/entry_points.txt,sha256=F8jVGaO3XCi60loecZlnq_5T4GnGpkzhnOE_GG0zzPo,54
39
+ agentltl_coding-0.2.4.dist-info/top_level.txt,sha256=U6DuI1edF6v8PK7oKaiHubdumNVs9btT8unBtnBqlwU,16
40
+ agentltl_coding-0.2.4.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ agentltl = agentltl_coding.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Laïla Elkoussy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ agentltl_coding