nanobot: 2026-09-11 15:17:08
This commit is contained in:
@@ -34,6 +34,10 @@ Never use `/tmp/`, hardcoded absolute paths, or in-memory databases for persiste
|
||||
|
||||
The exec safety guard blocks commands without an explicit workspace path (e.g. `lua -e '...'`, `which`). Write scripts to files inside the workspace (e.g. `tmp/script.lua`) and run them with `working_dir` set to the workspace root.
|
||||
|
||||
**Never put prose or user-supplied text into a command string** — not as an argument, not in a heredoc, not through a pipe. The guard scans the raw command string and has no shell parser, so quoting does not help. Any `X:` where `X` is an ASCII letter not preceded by another ASCII letter parses as a Windows drive path and blocks the whole command: Czech `Cíl:`, `Závěr:`, `směr:` all trip it (the diacritic before the letter defeats the guard's ASCII-only lookbehind), and so does `date '+%H:%M:%S'`. A literal `../` anywhere in the command — even inside prose — trips the traversal guard too.
|
||||
|
||||
Instead: `write_file` the text to `tmp/`, then pass the **path** (`--file tmp/x.md`, or `< tmp/x.md`). A path in the command is safe, and file tools are not subject to this guard. Skill CLIs that take text follow this — see `skills/project` and `skills/note`.
|
||||
|
||||
## python — use uv
|
||||
|
||||
For all Python code use `uv`, never `python` / `python3` / `pip` / `poetry` / `conda` directly. Details in `skills/python/SKILL.md`.
|
||||
|
||||
156
cron/jobs.json
156
cron/jobs.json
@@ -84,8 +84,8 @@
|
||||
"originMetadata": {}
|
||||
},
|
||||
"state": {
|
||||
"nextRunAtMs": 1789119381693,
|
||||
"lastRunAtMs": 1789112181683,
|
||||
"nextRunAtMs": 1789133781832,
|
||||
"lastRunAtMs": 1789126581821,
|
||||
"lastStatus": "ok",
|
||||
"lastError": null,
|
||||
"runHistory": [
|
||||
@@ -154,11 +154,23 @@
|
||||
"status": "ok",
|
||||
"durationMs": 10,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789119381802,
|
||||
"status": "ok",
|
||||
"durationMs": 17,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789126581821,
|
||||
"status": "ok",
|
||||
"durationMs": 11,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAtMs": 1789032980434,
|
||||
"updatedAtMs": 1789112181693,
|
||||
"updatedAtMs": 1789126581832,
|
||||
"deleteAfterRun": false
|
||||
},
|
||||
{
|
||||
@@ -185,77 +197,11 @@
|
||||
"originMetadata": {}
|
||||
},
|
||||
"state": {
|
||||
"nextRunAtMs": 1789113981697,
|
||||
"lastRunAtMs": 1789112181696,
|
||||
"nextRunAtMs": 1789133782309,
|
||||
"lastRunAtMs": 1789131982309,
|
||||
"lastStatus": "ok",
|
||||
"lastError": null,
|
||||
"runHistory": [
|
||||
{
|
||||
"runAtMs": 1789077981273,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789079781275,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789081581276,
|
||||
"status": "ok",
|
||||
"durationMs": 1,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789083381371,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789085181373,
|
||||
"status": "ok",
|
||||
"durationMs": 1,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789086981376,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789088781378,
|
||||
"status": "ok",
|
||||
"durationMs": 1,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789090581388,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789092381490,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789094181491,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789095981494,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789097781494,
|
||||
"status": "ok",
|
||||
@@ -309,11 +255,77 @@
|
||||
"status": "ok",
|
||||
"durationMs": 1,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789113981699,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789115781701,
|
||||
"status": "ok",
|
||||
"durationMs": 7,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789117581711,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789119381819,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789121181821,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789122981965,
|
||||
"status": "ok",
|
||||
"durationMs": 1,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789124781968,
|
||||
"status": "ok",
|
||||
"durationMs": 1,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789126582256,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789128382257,
|
||||
"status": "ok",
|
||||
"durationMs": 1,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789130182259,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"runAtMs": 1789131982309,
|
||||
"status": "ok",
|
||||
"durationMs": 0,
|
||||
"error": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"createdAtMs": 1789032980437,
|
||||
"updatedAtMs": 1789112181697,
|
||||
"updatedAtMs": 1789131982309,
|
||||
"deleteAfterRun": false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -48,3 +48,6 @@ Klíčová designová rozhodnutí: (1) brief je psaný pro čtenáře-agenta se
|
||||
- 2026-09-10: Self-analysis (na dotaz uživatele „proc jsi sám nevytvořil minimalistickou verzi?"): tři příčiny přepsání /handoff skillu. (1) Psal jsem pro špatného čtenáře — skill čte model, který potřebuje šablonu výstupu + constrainty, ne tréninkový dokument; defaultl jsem na dokumentační styl, protože většina mých artefaktů je pro člověka. Ukázková šablona > popis šablony. (2) Optimalizoval jsem na vypadání důkladně (Rules/Procedure jako zviditelněné uvažování) místo na funkci — uvažování patří do konverzace, ne do artefaktu. (3) Chyběl test „vyrobí tohle spolehlivě dobrý brief?", který se ověřuje použitím a ořezáváním, ne dodáním plné verze bez jediného běhu. Paradox: vlastní rule 4 skillu („be specific, not complete") jsem v samotném skillu porušil. Společný jmenovatel: rozhodnutí podle spotřebitele výstupu, ne podle vlastních zvyků. Diagnostika zrcadlová k Solarflare: tam runbook pro agenta místo člověka, tady pro člověka místo agenta. Pravidlo: před sepsáním skillu určit, kdo ho čte v momentě použití, a psát pro něj.
|
||||
- 2026-09-10: Prompting recept (na dotaz uživatele „jak to příště zadat, abych dostal lepší výstup?"): pro skládání skillů/artefaktů zadat 4 věci, které mě vedly k přepsání /handoffu: (1) kdo čte výstup v momentě použití (model vs člověk), (2) ukázka/kostra cílové podoby — „chci tohle, jen to napiš pořádně", (3) constraint na délku — „nejkratší verze, která funguje", (4) požadavek na ukázku použití („ukaž výstup na reálném příkladu") — tím se spustí test místo speculativního dodání plné verze. Poznámka pro mě: i bez zadání bych měl (1) a (4) udělat sám; (2) a (3) jsou legitimní požadavky na uživatele, protože definují chuť výstupu.
|
||||
- 2026-09-10: correction: Položku (1) „kdo čte výstup" z prompting receptu stahuji — u skillů plyne ze samotné definice (soubor pro model v momentě použití), není to legitimní zadání od uživatele, ale moje povinnost. Skutečná příčina: nezapátrání targetu PŘED psaním; default dokumentačního stylu vyhrál, protože čtenář nebyl první určující otázkou. Recept zredukován na 2 položky od uživatele (ukázka/kostra cílové podoby, constraint na délku) + 2 položky moje (čtenář z definice typu souboru, ukázka výstupu na reálném příkladu).
|
||||
- 2026-09-11: OpenWebUI zakázáno (2026-09-12) — žere sílně paměť a občas zasekne celý server. Jako náhradu chci vyzkoušet LibreChat, konkrétně self-hosted Docker variantu dle oficiální dokumentace:
|
||||
|
||||
https://www.librechat.ai/docs/local/docker
|
||||
|
||||
@@ -52,6 +52,13 @@ them. The Dream processor must not touch `notes/`.
|
||||
**Run scripts with `uv run`, workspace-relative paths** (exec runs from the workspace
|
||||
root, not the skill dir): `uv run skills/note/scripts/<script>.py …`.
|
||||
|
||||
**Never pass the text on the command line.** Not as an argument, not in a heredoc, not
|
||||
through a pipe — always `write_file` it to `tmp/` and pass the path. The exec safety
|
||||
guard scans the raw command string and has no shell parser, so quoting does not help: a
|
||||
colon right after a letter that follows a diacritic parses as a Windows drive path, and
|
||||
a note like `Cíl: koupit mléko` blocks the whole command with *path outside working dir*.
|
||||
Since capture takes the user's input verbatim, this would hit real notes, not edge cases.
|
||||
|
||||
**Language.** This skill body is English; always reply to the user in the user's own
|
||||
language.
|
||||
|
||||
@@ -60,9 +67,10 @@ language.
|
||||
The default: file the note into the knowledge base immediately, in this turn.
|
||||
|
||||
1. Read `Channel` / `Chat ID` from the runtime context if present.
|
||||
2. Capture:
|
||||
`uv run skills/note/scripts/note_capture.py --text "<raw input>" [--channel <ch>] [--chat-id <id>]`
|
||||
Pass the input **as-is** — do not reformulate or strip URLs here.
|
||||
2. Capture, in two steps — `write_file` the raw input to `tmp/note-capture.md`, then:
|
||||
`uv run skills/note/scripts/note_capture.py --file tmp/note-capture.md [--channel <ch>] [--chat-id <id>]`
|
||||
Pass the input **as-is** — do not reformulate or strip URLs here. See
|
||||
*Never pass the text on the command line* below for why it goes through a file.
|
||||
3. Run the **Compile workflow** (below) inline: acquire the lock, process `notes/inbox/`,
|
||||
file into `notes/notes.md`, move the source to `notes/done/` (or `notes/hard/`).
|
||||
4. **Commit** the change (see *Versioning* below): via `exec` run
|
||||
@@ -79,7 +87,10 @@ the user is firing off many notes quickly, suggest `/note cron`.
|
||||
Capture only; let the background cron file it later. Fast, non-blocking.
|
||||
|
||||
1. Read `Channel` / `Chat ID` from the runtime context if present.
|
||||
2. `uv run skills/note/scripts/note_capture.py --text "<raw input>" [--channel <ch>] [--chat-id <id>]`
|
||||
2. `write_file` the raw input to `tmp/note-capture.md`, then
|
||||
`uv run skills/note/scripts/note_capture.py --file tmp/note-capture.md [--channel <ch>] [--chat-id <id>]`
|
||||
(the extra write is what keeps the capture from being blocked — see below; it is still
|
||||
one fast turn, so this mode stays non-blocking)
|
||||
3. Confirm in **one short line** (e.g. "captured — I'll file it in the background") and **STOP the
|
||||
turn**. Forbidden here: reformulating, reading `notes/notes.md`, running any compile
|
||||
step, taking the lock. If you catch yourself about to read the doc, you are compiling
|
||||
@@ -174,5 +185,7 @@ never reached the KB — you may cancel it directly with `exec: rm notes/inbox/<
|
||||
|
||||
- `/note` with no content → ask what to note.
|
||||
- Empty / whitespace-only input → `note_capture.py` exits non-zero; ask for real content.
|
||||
- `No such file` from capture → the `write_file` step was skipped or the path is wrong;
|
||||
write `tmp/note-capture.md` first, never fall back to passing the text as an argument.
|
||||
- The compile step, not capture, decides sections and does all fetching. If you ever find
|
||||
yourself reformulating or reading `notes.md` during a `cron` capture, stop.
|
||||
|
||||
@@ -73,7 +73,9 @@ def _append_log(filename: str, body: str) -> None:
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Capture a raw note into notes/inbox/")
|
||||
parser.add_argument(
|
||||
"--text", default=None, help="Raw input; if omitted, read from stdin"
|
||||
"--file",
|
||||
default=None,
|
||||
help="Path to a file holding the raw input; if omitted, read from stdin",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--channel", default=None, help="Origin channel (telegram/websocket/cli)"
|
||||
@@ -83,7 +85,16 @@ def main() -> int:
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
body = args.text if args.text is not None else sys.stdin.read()
|
||||
# The text is never passed on the command line: the exec safety guard scans the
|
||||
# raw command string and misreads prose as a filesystem path (see SKILL.md).
|
||||
if args.file is None:
|
||||
body = sys.stdin.read()
|
||||
else:
|
||||
source = Path(args.file)
|
||||
if not source.is_file():
|
||||
print(f"No such file: {args.file}", file=sys.stderr)
|
||||
return 1
|
||||
body = source.read_text(encoding="utf-8")
|
||||
body = body.strip()
|
||||
if not body:
|
||||
print("Nothing to capture (empty input).", file=sys.stderr)
|
||||
|
||||
@@ -21,10 +21,13 @@ def workspace(tmp_path, monkeypatch):
|
||||
return tmp_path
|
||||
|
||||
|
||||
def _run(text=None, argv_extra=None):
|
||||
def _run(workspace, text=None, argv_extra=None):
|
||||
"""Build argv the way the skill does: text in a file, only its path in the command."""
|
||||
argv = ["note_capture.py"]
|
||||
if text is not None:
|
||||
argv += ["--text", text]
|
||||
source = workspace / "note-capture.md"
|
||||
source.write_text(text, encoding="utf-8")
|
||||
argv += ["--file", str(source)]
|
||||
if argv_extra:
|
||||
argv += argv_extra
|
||||
return argv
|
||||
@@ -52,7 +55,7 @@ def test_capture_writes_inbox_file_and_log(workspace, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
_run("koupit kanistr na vodu", ["--channel", "telegram", "--chat-id", "42"]),
|
||||
_run(workspace, "koupit kanistr na vodu", ["--channel", "telegram", "--chat-id", "42"]),
|
||||
)
|
||||
assert note_capture.main() == 0
|
||||
|
||||
@@ -76,14 +79,14 @@ def test_capture_writes_inbox_file_and_log(workspace, monkeypatch):
|
||||
|
||||
|
||||
def test_capture_no_leftover_tmp_files(workspace, monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", _run("neco"))
|
||||
monkeypatch.setattr(sys, "argv", _run(workspace, "neco"))
|
||||
assert note_capture.main() == 0
|
||||
tmp_files = list((workspace / "notes" / "inbox").glob(".*"))
|
||||
assert tmp_files == []
|
||||
|
||||
|
||||
def test_capture_omits_absent_provenance(workspace, monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", _run("bez kanalu"))
|
||||
monkeypatch.setattr(sys, "argv", _run(workspace, "bez kanalu"))
|
||||
assert note_capture.main() == 0
|
||||
content = next((workspace / "notes" / "inbox").glob("*.md")).read_text(
|
||||
encoding="utf-8"
|
||||
@@ -93,6 +96,46 @@ def test_capture_omits_absent_provenance(workspace, monkeypatch):
|
||||
|
||||
|
||||
def test_capture_empty_input_fails(workspace, monkeypatch):
|
||||
monkeypatch.setattr(sys, "argv", _run(" "))
|
||||
monkeypatch.setattr(sys, "argv", _run(workspace, " "))
|
||||
assert note_capture.main() == 1
|
||||
assert list((workspace / "notes" / "inbox").glob("*.md")) == []
|
||||
|
||||
|
||||
def test_capture_accepts_text_the_exec_guard_would_reject_on_a_command_line(
|
||||
workspace, monkeypatch
|
||||
):
|
||||
"""Regression: a note like `Cíl: …` parses as a Windows drive path in the exec guard.
|
||||
|
||||
Capture takes the user's input verbatim, so such text must reach the script
|
||||
through a file, never as an argument, a heredoc or a pipe.
|
||||
"""
|
||||
text = "Cíl: koupit mléko. Závěr: zítra."
|
||||
monkeypatch.setattr(sys, "argv", _run(workspace, text))
|
||||
|
||||
assert note_capture.main() == 0
|
||||
|
||||
content = next((workspace / "notes" / "inbox").glob("*.md")).read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert text in content
|
||||
|
||||
|
||||
def test_capture_rejects_missing_file(workspace, monkeypatch, capsys):
|
||||
monkeypatch.setattr(
|
||||
sys, "argv", ["note_capture.py", "--file", str(workspace / "chybi.md")]
|
||||
)
|
||||
|
||||
assert note_capture.main() == 1
|
||||
|
||||
assert "No such file" in capsys.readouterr().err
|
||||
assert list((workspace / "notes" / "inbox").glob("*.md")) == []
|
||||
|
||||
|
||||
def test_capture_no_longer_accepts_text_on_the_command_line(monkeypatch):
|
||||
"""`--text` is gone for good: prose in argv is what the exec guard blocks."""
|
||||
monkeypatch.setattr(sys, "argv", ["note_capture.py", "--text", "něco"])
|
||||
|
||||
with pytest.raises(SystemExit) as excinfo:
|
||||
note_capture.main()
|
||||
|
||||
assert excinfo.value.code == 2
|
||||
|
||||
Reference in New Issue
Block a user