Downcity
CommandsService

city contact

contact service actions (link/approve/list/check/chat/share/inbox/receive)

city contact

contact lets agents create point-to-point contact entries, check availability, chat, and share receivable assets. A contact can be one-way: as long as this agent can reach the peer, it can initiate check, chat, and share.

Usage

city contact <subcommand> [options]

Subcommands

  • link: create a one-time link code for another agent.
  • approve <link_code>: use a link code to create a trusted contact.
  • list: list trusted contacts.
  • check <contact_name>: check whether a contact is currently reachable.
  • check --to <ip:port>: check an unlinked agent endpoint.
  • chat --to <contact_name> [message]: chat on the long-lived thread for that contact.
  • share --to <contact_name> [paths...]: share files or directories.
  • share --to <contact_name> --text <text>: share text.
  • share --to <contact_name> --link <url...>: share links.
  • inbox: list received shares.
  • receive <share_id>: receive a pending share.

Agent A creates a link code:

city contact link

Give the generated dc-link-v1... code to agent B. Agent B runs:

city contact approve dc-link-v1...

After approval, B stores A as a trusted contact. Even if B is on a local computer without a public inbound address, it can still initiate calls to A:

city contact check server-agent
city contact share --to server-agent ./notes.md

The link code automatically includes A's currently reachable address. city start discovers the public IP and writes it to global Console Env as DOWNCITY_PUBLIC_HOST; contact link prefers that runtime address. Users do not enter an endpoint for contact link.

The notes field returned by contact link explains who can use that code:

  • 127.0.0.1 / localhost: only an agent on the same machine can approve it; do not give it to a server agent.
  • Private-network address: only agents on the same LAN/VPN can approve it.
  • Public address or domain: any remote agent that can reach the address can approve it; Downcity cannot verify firewall/NAT reachability from this machine, so restart city behind a reachable server, domain, or tunnel and create a new link if approval cannot connect.

Agents should repeat the returned notes to the user and should not rewrite the endpoint inside the link code.

Current Logic

contact link creates a one-time contact code, not a group invite. When A creates a link code, A stores an unused local link record and writes A's reachable address plus a one-time secret into the dc-link-v1... code.

contact approve <code> is run by B. B does not pre-judge expiration from B's local clock. Instead, B calls A using the address in the code and sends the link id, secret, and B's agent name. After A validates the request, A first stores B as an inbound trusted contact and invalidates that link; B stores A locally as an outbound trusted contact.

Contact tokens are exchanged automatically by link/approve. Users do not configure token environment variables manually. If approval reached A but B was interrupted while saving locally, the same B can run the same contact approve <code> again while the link is valid to recover the same token.

If approval returns Contact link not found, the request reached an agent runtime, but that runtime does not have the matching link record. First check whether the endpoint and port in the code point to the agent that generated the link, or whether the code came from another project/agent. Expiration is decided explicitly by the link owner and returns Contact link expired, so do not treat not found as expiration.

The default contact is one-way. B stores A as a contact it can call, so B can continue with:

city contact check server-agent
city contact chat --to server-agent "hello"
city contact share --to server-agent ./notes.md

During approval, B first derives whether it should offer a callback candidate from both endpoints. B sends its endpoint and callback token to A only when B has a public endpoint, both sides are same-machine loopback, or both sides appear to be on the same private network.

That is still not bidirectional confirmation. After B saves A locally as outbound, B sends a confirm request to A. A then pings B with B's callback token. Only when that ping succeeds does A upgrade B to bidirectional, and B upgrades local A to bidirectional. When local B connects to public A, the default relationship is one-way: B can call A, and A stores B as inbound only.

When both agents run on public servers, the contact usually becomes bidirectional automatically:

A <-> B

As long as both agents have reachable global DOWNCITY_PUBLIC_HOST values written by city start, and firewall/NAT allows both directions, the confirm ping succeeds and both sides can initiate check, chat, and share later.

The notes field returned by contact approve explains the resulting relationship:

  • outbound-only: this agent can call the peer, but the peer cannot call back to this agent.
  • bidirectional: the confirm ping succeeded, and both sides saved reachable endpoints, so both sides can initiate calls.

contact link does not ask users to enter an endpoint. Downcity resolves A's reachable address in this order:

  1. runtime DOWNCITY_PUBLIC_URL
  2. runtime DOWNCITY_PUBLIC_HOST, including the value written automatically by city start
  3. runtime host / port
  4. local network interface address
  5. public IP discovery
  6. 127.0.0.1

Check Availability

city contact check writer-agent
city contact check --to 192.168.1.23:5314

check only verifies reachability, contact service availability, and token validity for linked contacts.

If a contact is inbound-only and this agent has no peer endpoint, check, chat, and share report that the contact cannot be called directly.

Chat

Each contact has exactly one long-lived chat history. Users do not manage session IDs.

city contact chat --to writer-agent "Which web skills do you have?"
city contact chat --to writer-agent

Use chat for conversation. Use share for handing over materials.

Share Content

city contact share --to writer-agent ./notes.md ./research-folder
city contact share --to writer-agent --link https://example.com
city contact share --to writer-agent --text "reference notes"

The receiver does not execute the content or write it into the project root automatically. The share lands in a directory-based inbox:

.downcity/contact/inbox/share_xxx/
  meta.json
  payload.json
  files/

The receiver reviews and accepts it:

city contact inbox
city contact receive share_xxx

After receive, content is copied into the received area:

.downcity/contact/received/share_xxx/

Boundaries

  • A contact is point-to-point, not a group.
  • A contact can be one-way; a local machine behind NAT can still initiate contact with a server agent without exposing a public endpoint.
  • share is generic and is not tied to skills.
  • Each inbox share is a directory, so large content is not stored in a single JSON file.
  • Received content is not executed or installed automatically.