Remote coding agent: Telegram → home PC → GitHub Copilot CLI → local project. Single user, self-hosted, zero recurring infrastructure cost.
- Zero recurring cost. Do not add any dependency or service that costs money
or requires an external account. One runtime dependency (
grammy) is the budget; prefer Node built-ins (node:sqlite,node:test,process.loadEnvFile) over packages. - Never invent CLI flags, and never trust yesterday's answer. The Copilot CLI
auto-updates and its flags and model catalogue change. Verify against the
installed CLI (
copilot --help,help permissions,help config,help sandbox,help limits). Appending--agent __nope__to a full argv is a zero-cost way to check that every other flag parses. - Never weaken the security model.
--yolo,--allow-all,--allow-all-pathsand--allow-all-urlsmust never be passed. The child environment is an allow-list, not a deny-list. Additions to the permission deny-list are welcome; removals need a strong reason. - Never let secrets escape. Everything sent to Telegram, written to a log,
or stored in the database goes through
redact(). - Every agent role costs credits, so decisions about agents must not.
Classification, budgeting, confidence scoring and escalation live in
src/orchestrator/and are plain TypeScript. Never add an AI call whose only job is to decide whether to make another AI call. Free gates (tests, build, manifest integrity, repo scan) always run before a paid review. - Advisory roles must be read-only at the tool level. The explorer and
reviewer get
--deny-tool=write; their safety must not depend on the prompt being obeyed. - A provider must never silently lower the bar. The deny-list is written in
each CLI's own flag language, so a provider that cannot express "deny
shell(curl)" would void a protection the README still promises. Every provider
DECLARES its capabilities,
REQUIRED_CAPABILITIESare mandatory, andselectProvider()refuses rather than degrades. Declare a capability only when a real, verified flag on the installed CLI implements it. - "Zero recurring cost" is per-provider. It is true of Copilot because the
subscription already exists. Any other provider bills separately and must say
so in
billing, whichdoctorprints. - Anything that runs project-supplied code gets
buildChildEnv(), neverprocess.env.execCommand'senvREPLACES the environment; a hostile"test"script must not be able to read the bot token. - Treat the target repository as hostile configuration, not just hostile text.
The Copilot CLI loads agents, skills, hooks, plugins, MCP and LSP config
relative to the working directory. New capability paths go in
src/security/repoScan.ts; anything that names a command isblocking. - Git executes what
.git/configand.git/hooksname. New git invocations must go throughGit.run()so they inheritGIT_HARDENING, and anything that can execute belongs infingerprintGitControlSurface. - Never destroy user work. Git checkpointing must stay non-destructive: it
writes through a temporary
GIT_INDEX_FILEand must not touch the working tree or the user's index. Never overwrite an existing checkpoint ref. - Never block a Telegram handler on something another update must resolve. grammY processes updates strictly sequentially, so awaiting an approval inside a handler deadlocks the entire bot.
- Interfaces are thin clients. Telegram and the web UI submit, cancel,
retry and approve ONLY through
TaskService/ApprovalService, and observe ONLY through the repository and theEventBus. A queue cap, risk gate or approval check implemented inside one interface is a bug even when it works: it is where the interfaces start to disagree. Each interface must be fully optional; the core runs with either or both. - The web surface trusts nothing. All DOM insertion via
textContent, CSP with no inline script, mutations require theX-CodeRelayheader plus same-originOrigin, static serving only fromweb/with an extension allow-list. The browser gets project metadata, never filesystem paths.
npm run build # tsc
npm run lint # tsc with noUnusedLocals/Parameters/ImplicitReturns
npm test # lint + build + 306 tests (no AI calls, no credits)
npm run typecheck
npm run doctor
npm run agent -- <command>
node scripts/live-acceptance.mjs # SPENDS ~1-2 AI CREDITS. Manual only.
scripts/live-acceptance.mjs drives the real Copilot CLI against a throwaway
repo containing a real bug and asserts the whole chain: checkpoint -> repo scan
-> Copilot -> verification -> approval -> commit, plus "the bot token never
reached Telegram". Run it after touching the runner, permissions, git handling
or execCommand. The mocked suite cannot catch environment or quoting faults;
this is the only thing that can.
- TypeScript, ESM,
NodeNextresolution — always use.jsextensions in relative imports. - Tests use
node:test+node:assert/strict. No test framework dependency. - Untrusted text (anything from Telegram) is passed as an
argvelement withshell: false. On Windows,shell: trueroutes throughcmdExeInvocation, which quotes every token that needs quoting — never hand a raw string tospawn(shell: true). Do not quote a bare program name.cmd /c ""npm" "test""sets%0to a quoted bare name, so%~dp0insidenpm.cmdexpands to the current directory; npm then dies looking for<cwd>\node_modules\npm\bin\npm-prefix.js. That made every verification using npm/yarn/pnpm/gradlew fail and silently threw away correct work. - The model catalogue is not an entitlement.
--helpcan list a model that the API refuses at run time (Model "X" from --model flag is not available), and that refusal is often just transient rate limiting — the same model works again minutes later. Treat it as recoverable: switch models once, then fail with an actionable message. Never treat it as a fatal startup error. - Telegram messages are plain text — no
parse_mode— to avoid formatting injection when echoing code and paths. - Long-running child processes must be killed with
killProcessTreeand backed by a watchdog:closedoes not fire while a grandchild holds a stdio pipe. - Comments explain why, not what. One line where one line will do.
Any change to permissions, redaction, git safety, the state machine or the
approval flow must come with a test. The end-to-end suite (tests/e2e.test.ts)
drives the real runner against a mock Copilot CLI in a temporary git repo — extend
it rather than mocking the runner itself. tests/redteam.test.ts holds
regressions for attacks that were verified to work against an earlier build;
never weaken one to make a change pass.