Rules for coding agents in neuro-san. Follow them and make sure the checks in §6 pass on the first run.
- Agent networks are data-only HOCON files under neuro_san/registries/;
manifest.hoconcontrols whether a network isserved,publicly listed, and exposed viamcp— see manifest_hocon_reference.md. CodedTools live under neuro_san/coded_tools/, resolved viaAGENT_TOOL_PATH. Mirror the registry's folder layout when a tool belongs to a specific network.- No secrets in HOCON or code: read them from environment variables, or pass them per request via
sly_data(see Client-Provided API Keys). Never log or printsly_data. - Validate a HOCON file before opening a PR:
python -m neuro_san.client.hocon_validator_cli path/to/agent.hocon --verbose(see hocon_validator_cli.md). - Keep the diff focused: no unrequested refactors or reformatting of untouched files. Remove debug prints,
commented-out code and stray
TODOs before opening a PR. - Update the matching reference doc in the same PR when you change documented behavior: agent_hocon_reference.md, external_agents.md, mcp_tools.md, provider_tools.md, manifest_hocon_reference.md, llm_info_hocon_reference.md, toolbox_info_hocon_reference.md, mcp_service.md, clients.md, tests.md, or test_case_hocon_reference.md. Verify every link in a document you touch.
-
One module, one class. Each
.pyfile defines exactly one class (a handful of pre-existing exceptions are not precedent). Split helper logic into a separate module rather than adding a second class to an existing file. -
No module-level functions, no nested functions, no lambdas. Every function is a method (or
@staticmethod) on the module's class. If a method needs a helper, make it another method on the same class; bind arguments withfunctools.partialon a method or@staticmethodwhere a lambda would otherwise have been used. -
No comprehensions. Use explicit
forloops instead of list/dict/set/generator comprehensions, even when a comprehension would be shorter. -
Always use type hints. Every function/method signature needs type hints for all parameters and the return type; annotate a variable assignment too when the type isn't obvious from the right-hand side.
-
snake_case for functions, methods, variables, parameters and attributes; PascalCase for classes; UPPER_CASE for constants. Comment the reason if an external API forces
camelCase. -
Read dictionary values with
.get(), neverdict[key]; plain subscript is fine for assignment (config["key"] = value). -
Catch the specific exceptions that can be handled at that level. Pylint flags a broad
except Exception(W0718); where one is genuinely needed, add# pylint: disable=broad-exception-caughtand state why in a comment. -
Never fail silently. Report a missing/unreadable file, malformed input, or an unknown choice with the full exception, and log enough to act on.
-
Prefer
loggeroverprint, with lazy%formatting (logger.info("Loaded %s", name)) rather than f-strings. -
Use accessors, not internals: do not read another class's attributes directly and do not
isinstanceagainst a concrete class — check the interface, or add an interface method that answers the question. -
Docstrings on every class, function and method. A class docstring explains its purpose and responsibilities. A function/method docstring covers what it does, its parameters, its return value, and any exception it raises, in this exact format:
""" <summary of what the method does> :param <name>: <description> :param <name>: <description> :return: <description> """One
:paramline per parameter, in order, using its exact name. Omit:return:only when the method returnsNone. Add:raises <ExceptionType>: <description>after:return:if the method can raise something worth documenting. Write docstrings and comments in present tense: what the code or test does today, not its history (no "used to ...", "Previously ..."). -
Mark overridden methods with
@override(from typing_extensions import override). Comment the non-obvious: threading/lifecycle behavior and the reason behind a design decision — not what a line already says.
- One test module per class, named after it. Tests for
some_module.py(classSomeClass) live intest_some_module.pyas the single classTestSomeClass, mirroring the source path undertests/. Everytests/directory that holds test modules needs an__init__.py, or the CI pylint gate silently skips it — see tests.md. - Test classes derive from
unittest.TestCase, orunittest.IsolatedAsyncioTestCasewhen the class hasasync deftests.IsolatedAsyncioTestCaseruns its own async tests, so do not add@pytest.mark.asyncio. - Use
TestCaseassert methods (assertEqual,assertIs,assertIsNone,assertIn,assertRaises, and so on), never a bareassert— they report both operands on failure. - The Code style rules in §2 apply to tests too: helpers are methods or
@staticmethods, never module-level or nested functions, and neverlambdas — bind a mockside_effectwithfunctools.partialon a@staticmethodinstead. - Run the basic suite before opening a PR:
pytest -v -m "not integration and not smoke and not needs_server" -n auto. Narrow to one test withpytest -v path/to/test_file.py::TestClassName::test_method_name. Full marker/command reference: tests.md.
- Don't make assumptions. Before describing how something works, fixing a bug, or claiming a test passes or fails, read the relevant source or run the test. Never guess from naming conventions or memory of similar code.
- Always cite a reference when reporting. A factual claim about the codebase ("this function does X", "this bug is caused by Y") needs the file path and line number(s) backing it. A test result needs the actual command run and its output, not a summary from assumption.
- Branch off
mainwith a short, descriptive branch name; never commit directly tomain. - Never
git add,git commitorgit pushunless explicitly told to. Finishing a change, fixing a bug, or completing a feature is not implicit permission. Leave changes unstaged so the reviewer can inspect them first.
python -m neuro_san.client.hocon_validator_cli path/to/agent.hocon --verbose # HOCON structure, no LLM calls
flake8 neuro_san tests # style, line length 120
build_scripts/run_pylint.sh # pylint --rcfile=.pylintrc
export AGENT_TOOL_PATH="./neuro_san/coded_tools"
export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}."
pytest -v -m "not integration and not smoke and not needs_server" -n auto # basic unit testsNotes:
- Line length is 120 for both Python (flake8/pylint) and Markdown.
- To include the
needs_servertests, start a server first (python -m neuro_san.service.main_loop.server_main_loop) and dropand not needs_server.build_scripts/server_start.shdoes this in CI, but itsapt-getstep only works inside the CI container. build_scripts/run_markdownlint.sh(pymarkdown --config ./.pymarkdownlint.yaml scan) only scans./docsand./README.md. Any other Markdown file, this one included, needspymarkdown --config ./.pymarkdownlint.yaml scan <path>run by hand.build_scripts/run_shellcheck.shrunsshellcheckon every*.shfile it finds, skipping paths that containleaf-common.- Tests marked
integrationorsmokeneed extra environment variables and are not run on every push — see tests.md for the full breakdown.