Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2, AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code changes across packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
272 lines
9.6 KiB
Python
Executable File
272 lines
9.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Bootstrap AirNDB network debugging context files."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Dict, Tuple
|
|
|
|
from airndb_capture import ensure_capture_tool
|
|
|
|
MARKER_BEGIN = "<!-- AIRNDB:BEGIN -->"
|
|
MARKER_END = "<!-- AIRNDB:END -->"
|
|
|
|
|
|
def now_iso() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
def airndb_agents_block() -> str:
|
|
return f"""{MARKER_BEGIN}
|
|
## AirNDB Network Debug Workflow
|
|
|
|
1. Use AirNDB for `/airndb` sessions that need tcpdump/WinDump packet capture, pcap reading, BPF filters, or network evidence.
|
|
2. On first entry, detect tcpdump/WinDump; on Windows, if none is available, download official `WinDump.exe` from https://www.winpcap.org/windump/install/, verify SHA1, and write `AirPlan/state/airndb/tool.env`.
|
|
3. Only capture authorized traffic; prefer short bounded captures with `-nn`, `-s 0`, `-c <count>`, and narrow BPF filters.
|
|
4. Before capture, load:
|
|
- `AirPlan/AGENTS.md`
|
|
- `AirPlan/docs/architecture/adr/`
|
|
- `AirPlan/docs/architecture/c4/module.md`
|
|
- `AirPlan/docs/network/airndb-log.md`
|
|
5. Store pcap, text summaries, and JSON reports under `AirPlan/docs/network/airndb-captures/`.
|
|
6. Record exact command, interface, filter, capture window, artifact paths, key observations, and residual risk in `AirPlan/docs/network/airndb-log.md`.
|
|
7. AirDbg may call AirNDB when debugging needs DNS/TCP/UDP/TLS/HTTP, ports, proxy, firewall, packet-loss, retransmit, reset, or pcap evidence.
|
|
8. Update ADR/C4 when network boundaries, capture tooling, observability, ports, protocols, DNS, proxy, TLS, or runtime topology become durable architecture context.
|
|
{MARKER_END}
|
|
"""
|
|
|
|
|
|
def c4_module_template() -> str:
|
|
return """# C4 Module
|
|
|
|
## System Context
|
|
- TODO: Describe the system, users, and important external systems.
|
|
|
|
## Containers
|
|
- TODO: Describe runtime/deployable units and network boundaries.
|
|
|
|
## Modules
|
|
|
|
| Module | Responsibility | Network Interfaces | Dependencies | Data Ownership | Network Debug Notes |
|
|
| --- | --- | --- | --- | --- | --- |
|
|
| TODO | TODO | TODO | TODO | TODO | TODO |
|
|
|
|
## Network / Observability Boundaries
|
|
- tcpdump/WinDump capture points, interfaces, container/WSL/VM/host boundaries: TODO
|
|
- Ports, protocols, DNS, proxy, TLS, firewall, NAT, or gateway notes: TODO
|
|
|
|
## Change Log
|
|
- TODO: Record network-boundary or observability changes discovered by AirNDB.
|
|
"""
|
|
|
|
|
|
def adr_template() -> str:
|
|
return """# ADR-0001: AirNDB Packet Capture Governance
|
|
|
|
- Status: Accepted
|
|
- Date: TODO
|
|
|
|
## Context
|
|
Network debugging needs durable, AI-readable packet capture context and bounded evidence collection.
|
|
|
|
## Decision
|
|
Use AirNDB to build safe tcpdump/WinDump commands, capture or read pcap artifacts, and maintain `AirPlan/docs/network/airndb-log.md`, C4 module docs, and ADR records when network boundaries or diagnostics change.
|
|
|
|
## Consequences
|
|
- Packet evidence can be reused by future AirDbg or AirNDB sessions.
|
|
- Captures must stay bounded and authorized.
|
|
- pcap artifacts may contain sensitive data and need careful handling.
|
|
|
|
## Alternatives
|
|
- Chat-only packet notes: rejected because commands, filters, and pcap paths are easy to lose.
|
|
"""
|
|
|
|
|
|
def network_log_template() -> str:
|
|
return """# AirNDB Network Debug Log
|
|
|
|
Append entries for AirNDB packet capture sessions.
|
|
|
|
## Entry Template
|
|
|
|
### YYYY-MM-DD: short network issue title
|
|
|
|
- Scope / authorization: TODO
|
|
- Symptom: TODO
|
|
- Interface: TODO
|
|
- Filter: TODO
|
|
- Capture window: TODO
|
|
- Command: TODO
|
|
- Artifacts: TODO
|
|
- Key observations: TODO
|
|
- AirDbg handoff: TODO
|
|
- ADR/C4 updates: TODO
|
|
- Residual risk: TODO
|
|
"""
|
|
|
|
|
|
def captures_gitignore_template() -> str:
|
|
return """*.pcap
|
|
*.pcapng
|
|
*.cap
|
|
*.txt
|
|
*.json
|
|
!.gitignore
|
|
"""
|
|
|
|
|
|
def airndb_gitignore_template() -> str:
|
|
return """tool.env
|
|
"""
|
|
|
|
|
|
def write_if_missing(path: Path, content: str) -> bool:
|
|
if path.exists():
|
|
return False
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(content, encoding="utf-8", newline="\n")
|
|
return True
|
|
|
|
|
|
def upsert_agents_md(path: Path) -> str:
|
|
block = airndb_agents_block().rstrip() + "\n"
|
|
if path.exists():
|
|
original = path.read_text(encoding="utf-8")
|
|
existed = True
|
|
else:
|
|
original = "# AGENTS.md\n\n"
|
|
existed = False
|
|
|
|
begin = original.find(MARKER_BEGIN)
|
|
end = original.find(MARKER_END)
|
|
if begin >= 0 and end > begin:
|
|
end += len(MARKER_END)
|
|
updated = original[:begin].rstrip() + "\n\n" + block + original[end:].lstrip()
|
|
status = "updated"
|
|
else:
|
|
updated = original.rstrip() + "\n\n" + block
|
|
status = "updated" if existed else "created"
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(updated, encoding="utf-8", newline="\n")
|
|
return status
|
|
|
|
|
|
def artifact_map(project_root: Path) -> Dict[str, Path]:
|
|
return {
|
|
"AGENTS.md": project_root / "AirPlan" / "AGENTS.md",
|
|
"c4_module": project_root / "AirPlan" / "docs" / "architecture" / "c4" / "module.md",
|
|
"adr_dir": project_root / "AirPlan" / "docs" / "architecture" / "adr",
|
|
"adr_0001": project_root / "AirPlan" / "docs" / "architecture" / "adr" / "ADR-0001-airndb-packet-capture-governance.md",
|
|
"network_log": project_root / "AirPlan" / "docs" / "network" / "airndb-log.md",
|
|
"captures_dir": project_root / "AirPlan" / "docs" / "network" / "airndb-captures",
|
|
"captures_gitignore": project_root / "AirPlan" / "docs" / "network" / "airndb-captures" / ".gitignore",
|
|
"tool_env": project_root / "AirPlan" / "state" / "airndb" / "tool.env",
|
|
"airndb_gitignore": project_root / "AirPlan" / "state" / "airndb" / ".gitignore",
|
|
"state": project_root / "AirPlan" / "state" / "airndb" / "state.json",
|
|
}
|
|
|
|
|
|
def write_state(path: Path, enabled: bool, project_root: Path) -> None:
|
|
artifacts = artifact_map(project_root)
|
|
health = {
|
|
name: artifacts[name].exists()
|
|
for name in [
|
|
"AGENTS.md",
|
|
"c4_module",
|
|
"adr_dir",
|
|
"adr_0001",
|
|
"network_log",
|
|
"captures_dir",
|
|
"captures_gitignore",
|
|
"tool_env",
|
|
"airndb_gitignore",
|
|
]
|
|
}
|
|
payload = {
|
|
"enabled": enabled,
|
|
"updatedAt": now_iso(),
|
|
"projectRoot": str(project_root),
|
|
"artifactHealth": health,
|
|
"captureTool": ensure_capture_tool(project_root, auto_install=False),
|
|
}
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
|
|
|
|
def enter_mode(project_root: Path) -> Tuple[str, Dict[str, str]]:
|
|
artifacts = artifact_map(project_root)
|
|
results: Dict[str, str] = {}
|
|
results["AGENTS.md"] = upsert_agents_md(artifacts["AGENTS.md"])
|
|
results["c4_module"] = "created" if write_if_missing(artifacts["c4_module"], c4_module_template()) else "exists"
|
|
artifacts["adr_dir"].mkdir(parents=True, exist_ok=True)
|
|
results["adr_dir"] = "exists"
|
|
results["adr_0001"] = "created" if write_if_missing(artifacts["adr_0001"], adr_template()) else "exists"
|
|
results["network_log"] = "created" if write_if_missing(artifacts["network_log"], network_log_template()) else "exists"
|
|
artifacts["captures_dir"].mkdir(parents=True, exist_ok=True)
|
|
results["captures_dir"] = "exists"
|
|
results["captures_gitignore"] = "created" if write_if_missing(artifacts["captures_gitignore"], captures_gitignore_template()) else "exists"
|
|
results["airndb_gitignore"] = "created" if write_if_missing(artifacts["airndb_gitignore"], airndb_gitignore_template()) else "exists"
|
|
tool_status = ensure_capture_tool(project_root, auto_install=True)
|
|
results["capture_tool"] = tool_status.get("status", "unknown")
|
|
results["capture_tool_path"] = tool_status.get("tool", "")
|
|
results["capture_tool_source"] = tool_status.get("source", "")
|
|
results["capture_tool_env"] = tool_status.get("projectEnv", "")
|
|
if tool_status.get("driverHint"):
|
|
results["capture_driver_hint"] = tool_status["driverHint"]
|
|
write_state(artifacts["state"], True, project_root)
|
|
return "enabled", results
|
|
|
|
|
|
def exit_mode(project_root: Path) -> Tuple[str, Dict[str, str]]:
|
|
artifacts = artifact_map(project_root)
|
|
write_state(artifacts["state"], False, project_root)
|
|
return "disabled", {}
|
|
|
|
|
|
def status_mode(project_root: Path) -> Tuple[str, Dict[str, str]]:
|
|
artifacts = artifact_map(project_root)
|
|
state_file = artifacts["state"]
|
|
enabled = False
|
|
if state_file.exists():
|
|
try:
|
|
payload = json.loads(state_file.read_text(encoding="utf-8"))
|
|
enabled = bool(payload.get("enabled"))
|
|
except json.JSONDecodeError:
|
|
enabled = False
|
|
results = {
|
|
name: ("ok" if path.exists() else "missing")
|
|
for name, path in artifacts.items()
|
|
if name != "state"
|
|
}
|
|
return ("enabled" if enabled else "disabled"), results
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Manage AirNDB network debug artifacts.")
|
|
parser.add_argument("--mode", choices=["enter", "setup", "exit", "status"], default="enter")
|
|
parser.add_argument("--project", default=".")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> None:
|
|
args = parse_args()
|
|
project_root = Path(args.project).expanduser().resolve()
|
|
if args.mode in {"enter", "setup"}:
|
|
mode_state, result = enter_mode(project_root)
|
|
elif args.mode == "exit":
|
|
mode_state, result = exit_mode(project_root)
|
|
else:
|
|
mode_state, result = status_mode(project_root)
|
|
print(f"airndb_mode={mode_state}")
|
|
print(f"project_root={project_root}")
|
|
for key, value in result.items():
|
|
print(f"{key}={value}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|