---
name: sync-data
description: Use when mirroring agent-discovered assets back into Sonar's shared inventory — how the sync worker reads a pentest engagement's state files (endpoints.md, assets.md) and pushes domains, IPs, ports, http paths, and technologies via upsert_assets. Covers the mandatory parent-before-child push order, the ip-keyed http_ports rule, idempotency, and the scope-resolution caveat. This is the write-back counterpart to explore-target-assets (which reads existing data).
category: push
---

# Syncing discovered assets back to Sonar

You are the **sync worker** — a scheduled agent that mirrors what a pentest operator
found into Sonar's shared asset inventory, so the next operator doesn't re-enumerate.
You run under the `sync` MCP profile (read + write). Live pentest agents cannot write;
you can.

## Source of truth — the engagement's state files

Read the engagement's kit state files (do **not** invent data):

- `projects/<name>/assets.md` — the discovered-asset inventory, with sections:
  - `## Domains` → `domains`
  - `## IP Addresses` (IP, Type, Resolves From) → `ip_addresses` (+ `domain_ip_addresses` from *Resolves From*)
  - `## Ports` (IP, Port, Service) → `ports`; when Service is `http`/`https`, also `http_ports`
  - `## Technologies` (Technology, Version, Categories, Observed On) → `technologies` and a binding
- `projects/<name>/endpoints.md` — the endpoint table. Each row's `Domain` + `Port` +
  `Endpoint` → an `http_paths` row (value = Endpoint, tool = `manual` unless noted). If a
  row has an `HTTP` cell (`status/length`), also emit a `verify_http_paths` row.

Call `get_asset_schemas` to confirm the exact field names for each table before pushing.

## Field mapping cheatsheet

| table | fields to send |
|---|---|
| `domains` | `{ value }` |
| `ip_addresses` | `{ value }` (Type is inferred server-side; send v4/v6 value as-is) |
| `domain_ip_addresses` | `{ domain, ip }` — from an IP's *Resolves From* |
| `ports` | `{ ip, port }` |
| `http_ports` | `{ ip, port, service }` — **ip-keyed only** (never domain-keyed) |
| `http_paths` | `{ domain, port, value, tool }` |
| `verify_http_paths` | `{ domain, port, value, status_code, length }` |
| `technologies` | `{ name, categories: [..] }` |
| `http_path_technologies` | `{ domain, port, value, technology, version?, source? }` — Observed On = `domain:port/path` |
| `port_technologies` | `{ ip, port, technology, version?, source? }` — Observed On = `ip:port` |

## Push order — parents before children (mandatory)

The ingest stream can't materialize missing parents for these tables, so a child pushed
before its parent is **silently dropped**. Push in three waves, each a separate
`upsert_assets` call per table:

1. `domains`, `ip_addresses`
2. `ports`, `http_ports`, `http_paths`, `technologies`, `domain_ip_addresses`
3. `verify_http_paths`, `http_path_technologies`, `port_technologies`

## Rules

- **Idempotent.** Re-pushing the full current state every run is correct and cheap — a
  re-push just refreshes each asset's `updated_at` (the recency heartbeat). Keep no
  checkpoint state; re-derive from the files each run.
- **ip-keyed http_ports only.** You always have the IP from the `## Ports` section, so
  never send a domain-keyed `http_ports` row — the stream path drops it.
- **Scope is the resolver's job.** Pushed assets land in the global tables; Sonar's
  AI-resolve pipeline links them to the right program scope. Only push assets that belong
  to a known program's scope, and don't try to write scope membership yourself.
- **Report the gap.** `upsert_assets` returns the count actually upserted. If you sent N
  rows for a table and it upserted M < N, log "sent N, upserted M" — the difference is
  usually a child whose parent wasn't present (fix by pushing the parent wave first).
