Skip to content

Assync Security Model

Product Boundary

Assync is an external development control plane. It must never become a runtime, build, deployment, or operational dependency of any managed project. The mandatory invariant: if Assync is removed completely, every managed project must still build, test, deploy, and run without modification. Verified structurally in this codebase: tests/test_isolation.py::NoManagedProjectDependencyTests asserts that src/assync/** imports nothing beyond its own package, its three declared dependencies (typer, rich, pyyaml), and the standard library — never a managed project's own code.

Architecture Layering

CLI            Web API (future)      GitHub Integration (future)
        \              |              /
         v             v             v
              Application Services
                       |
                       v
       Domain  +  Storage  +  Adapters

Business logic lives in the service modules (missions/service.py, runs/service.py, orchestrator/service.py, etc.), never directly in CLI handlers (commands/*.py) — so a future Web UI or GitHub App can call the same services the CLI does, without duplicating logic. No web server is required or implemented in this MVP.

Capability Enforcement

See authorization-model.md for the full model. The security-relevant invariant: effective capabilities are calculated centrally (authorization/policy.py::effective_capabilities) as an intersection — no adapter, role, or synthesis result can grant itself a capability.

Subprocess Safety (adapters/local_command.py)

  • Never shell=True — arguments are always an explicit list.
  • Executable allowlist, checked before every execution.
  • git push/merge/clean hard-denied regardless of granted capabilities.
  • Explicit timeout; output capped at 100,000 characters; a credential-pattern regex redacts api_key=/secret_key=/password=/ token=-shaped values from captured output before it is ever persisted or displayed.
  • Only a minimal environment allowlist (PATH, HOME, LANG, LC_ALL plus explicit caller-approved keys) is passed to the child process — never the full process environment, so ambient secrets are not inherited by default.

Filesystem Safety (storage/filesystem.py, local_command.py)

Every record ID and every resolved working directory is checked to remain under its declared root before use — traversal is rejected outright. projects/service.py::register_project additionally rejects a new project path that is nested inside (or contains) an already-registered project.

Prompt-Injection Boundary

This MVP calls no remote LLM, so there is no live prompt-injection surface today — but the request/response contract is designed so that repository or agent-output content can never alter: authorization, capability grants, role identity, allowed paths, the output schema, or human decision authority. Those are all fixed by the orchestrator before adapter.execute() is called, and a returned report is independently schema-validated (runs/report_schema.py) rather than trusted verbatim — a successful adapter exit with an invalid report is still recorded as a failed run (runs/service.py::execute_agent_run).

No Hidden Chain-of-Thought

AgentRunResult and the structured report schema only ever carry summary, recommendation, confidence, findings, evidence, assumptions, limitations, and required_human_decisions — never a raw reasoning transcript.

Two BLOCKED Concepts — Deliberately Distinct

RuntimeState.BLOCKED (an agent/mission execution state) and EvidenceClassification.BLOCKED (a specific claim's verification could not be completed) intentionally share the literal string "BLOCKED" — this mirrors .assync/runtime-visibility.md's own documented distinction. Because both are str, Enum subclasses, Python considers RuntimeState.BLOCKED == EvidenceClassification.BLOCKED to be True by value; this codebase never compares across the two enum types, and this exact behavior is documented and asserted directly in tests/test_core.py (see "Known MVP Limitations" below).

Known MVP Limitations

  • No production credentials, autonomous production actions, or release execution exist anywhere in this codebase — by omission, not by a runtime guard that could be bypassed. There is nothing to bypass.
  • The credential-redaction regex in local_command.py is a defense-in-depth measure, not a guarantee — callers must still avoid passing secrets into adapter input in the first place.
  • No sandboxing (containers, seccomp, etc.) is applied to the local command adapter beyond the allowlist/timeout/output-cap controls described above.