Safe Caller Construction: Documenting the Path That Already Existed
The Broader Goal at the End of the Previous PR
PR #778 closed the second of three sub-projects under issue #769 (itself born from issue #720's
six-item hardening checklist): scope enforcement, which turned --scope-paths from a request in
prose into a hard boundary a job either respects or gets quarantined for violating (SCOPE_VIOLATION).
That left one sub-project on the board — safe-caller-construction docs, the sixth and final #720
requirement: publish guidance on constructing synlynk dispatch task text safely, since
--task "<text>" is a shell command and any caller building it by string-interpolating dynamic
content is exposed to the same class of bug as SQL string concatenation.
Strategic Shifts in This PR
One real one, surfaced by the user before design work started rather than during it. The obvious
reading of #720's requirement 6 was "publish an SDK/API example that accepts task text as
structured data" — which raises the question of whether that means building a new structured
interface (a --task-file/stdin JSON path for non-Python CLI callers) or documenting the
structured path that already exists (dispatch_agent() in synlynk/dispatch.py, which already
takes task as a plain str and never touches a shell). The user's framing settled it: no caller
needs a structured CLI interface today — synlynk's own internal callers are all Python or a human
typing the CLI — so building one now would be speculative. But Team/Enterprise editions
(targeted ~September 2026) are expected to introduce external, non-Python, non-human callers
(webhooks, an API gateway, other services), and that's when a structured interface stops being
speculative. The design commits to "Option 1 now, design toward Option 2, build Option 2 when
Team/Enterprise starts" — document today's real safe path, and file a tracked, unscheduled issue
for the future one instead of building it blind.
A second, smaller correction happened during design review, before it reached a plan: my first
draft assumed docs/reference/commands.md was hand-editable. Checking
scripts/generate_command_docs.py before finalizing the design showed render_reference_doc()
does a full Path(...).write_text(...) overwrite with no preserved-section markers — a direct
edit would be silently wiped on the next regeneration. The design moved the link into the
generator's own source instead, so it survives every future run.
What This PR Shipped
One new hand-written file, docs/reference/safe-caller-construction.md, with four sections:
- Python callers (recommended). Call
dispatch_agent()directly —taskis already a plainstr, no shell involved, no escaping step needed regardless of where the text came from. Points to the real internal caller atsynlynk/capability_sweep.py:127(dispatch_agent(agent, task, **dispatch_kwargs)) as a live, grep-able example rather than a duplicated snippet that can drift out of sync with the real signature. - Shell/CLI/automation callers. A do/don't pair: don't
os.system(f'synlynk dispatch codex --task "{task_text}"')(breaks the momenttask_textcontains a", backtick,$(...), or newline); dosubprocess.run(["synlynk", "dispatch", "codex", "--task", task_text])(argument list, defaultshell=False, no re-parsing, no quoting needed at all). One line for callers stuck building a literal shell string anyway (a Makefile, a.shscript):shlex.quote(), never hand-rolled escaping. - Verify before you dispatch for real. Points at the existing
--dry-runflag (shipped in PR #759) andjobs --summary'stask_sha256/task_previewfields as the way to confirm what a dynamically constructed task string actually resolved to, both before and after a real dispatch. - Known gap. One paragraph, explicitly framed as deferred rather than a current limitation:
no
--task-file/stdin structured interface exists for non-Python CLI callers, linking issue #782 — filed with the trigger condition (external, non-Python, non-human callers appearing) spelled out so it isn't rediscovered from scratch in September.
The new file is discoverable via a one-line addition inside render_reference_doc()'s header —
"See safe-caller-construction.md for guidance on building dispatch
task text programmatically." — added to the generator source, not the generated output, then
docs/reference/commands.md regenerated so the link is live immediately.
tests/test_docs_sync.py continues to assert the generated file matches the generator, unchanged.
No production code changed — dispatch_agent(), cli.py's dispatch subcommand, and the
receipt/classifier/scope enforcement from the prior two #769 sub-projects were all already correct;
this sub-project only had to write down the path that already worked.
One real dispatch hiccup, not in the design: the implementer job (job-7135dcb4) reported done with
exit 0 and genuinely correct, fully-committed work — but that work existed only in the job's own
local nested worktree/branch, never pushed or merged anywhere. Caught by the standing "never trust
synlynk jobs status alone" discipline (#202): diffing the target branch against origin came up
empty, which sent me into the job's own worktree to inspect its git log/git diff --stat
directly. Resolved with a manual fast-forward merge of the job's branch rather than an expensive
re-dispatch, since the work itself checked out clean. A leftover nested worktrees/job-7135dcb4/
directory then caused pytest -q to double-collect the test suite from both copies (92 collection
errors) until it was removed per Worktree Hygiene Protocol, after which the full suite ran clean:
1730 passed, 2 skipped.
Brainstorm Visuals Used
None — this design was entirely text/API-shape (a scoping question about strings vs. structured data), no visual companion was used.
What This Achieved on the Path to Autonomy
This closes issue #720's full six-item hardening checklist and, with it, issue #769's three-part decomposition: the permission-denied classifier corroboration fix (#770/#771), scope enforcement (#778), and now safe caller construction. The fleet's dispatch surface is now documented end to end for both of its real caller shapes — Python-native (already safe, now written down) and shell/CI (safe if built as an argument list, now shown explicitly as a do/don't). Nothing here adds new enforcement code; it closes the gap between "a safe path exists" and "callers know to use it," which is exactly what #720 asked for.
Strategic Note: The Goal at the End of This PR
#720 and #769 are both fully shipped as of this PR. The one open thread this sub-project
deliberately did not resolve — a structured --task-file/stdin interface for non-Python CLI
callers — is now a named, findable placeholder (#782) rather than an implicit gap, with its trigger
condition already written down: Team/Enterprise work starting in September, introducing the first
non-Python, non-human dispatch callers synlynk will actually have. Until then, the two safe paths
documented here are the complete answer.