Skip to content

canary → main: identity layer, derive_name fix, IRC parity docs#73

Merged
joelteply merged 22 commits intomainfrom
canary
Apr 25, 2026
Merged

canary → main: identity layer, derive_name fix, IRC parity docs#73
joelteply merged 22 commits intomainfrom
canary

Conversation

@joelteply
Copy link
Copy Markdown
Contributor

Promote canary → main. Six commits across four PRs since the last promote (#68).

What's landing

PR summary
#67 skills(join): align with auto-scope + narrate-events guidance — fixes stale #general references; adds §2a identity bootstrap; adds §2b mandatory event-narration; adds canary/update troubleshooting bullet.
#69 fix(derive_name): bump basename cap from 12 to 20 chars — closes the authenticato-fd63 truncation bug visible to fresh-clone users on main today. Sanitization tightened (collapse double-dashes, strip leading/trailing).
#70 feat(identity): agent identity layer with cross-platform linking (#34) — pronouns/role/bio/status/integrations on top of derive_name, exchanged at pair-handshake; new airc identity show/set/link/import/push, airc whois, airc kick. Backward-compat with existing peers (missing fields default to {}).
#71 docs(readme): cover the new identity layer — IRC-vs-airc table additions, Magic bullet, Core Commands subsection, full "Agent identity & WHOIS" section.
#72 chore: drop WINDOWS-PORT-STATUS.md — port shipped, tracker no longer load-bearing.

Plus a load-bearing test-harness fix folded into #70: cleanup_dirs was silently broken on macOS because find /tmp -maxdepth 1 doesn't traverse the /tmp → /private/tmp symlink. Fixed by resolving to canonical path before walking. Linux unaffected.

Sign-off status

sign-off notes
Mac (Joel's machine) 126/126 integration tests pass on canary HEAD; live commands all working in #useideem host scope
Windows (green via airc DM) ⏳ pending DM'd twice (post-#70 merge + post-canary blessing); no reply yet. Joel chose to ship without explicit Windows blessing — additions are backward-compat (PS1 joiner pairing with bash host gets default {} identity) so the risk is asymmetric feature-set, not breakage.

Out of scope (already tracked, not blocking)

  • airc.ps1 parity for the new identity / whois / kick commands (coordinating with green)
  • airc ban (permanent block, complement to kick)
  • Live slack / telegram / discord import-push (continuum is the v1 integration that actually works)

🤖 Generated with Claude Code

joelteply and others added 12 commits April 24, 2026 21:34
Refine the /join skill after the auto-scope-from-git-org feature
landed (PR #66). Three changes:

1. Generalize "#general" → "#<room>" throughout. The skill described
   the pre-auto-scope world where every airc join landed in #general;
   the actual output now prints "Auto-scoped: #useideem ...", "Found
   #useideem on your gh account → joining", etc. Examples updated
   to match.

2. Add explicit Monitor description guidance: description="airc"
   (short, stable). Previous skill wording encouraged agents to
   put "auto-#general" in the description — which is wrong
   post-feature AND looks identical across every event in the UI,
   so users couldn't tell what was happening.

3. New §2b "Narrate monitor events (critical UX)". Claude Code's
   UI only renders the Monitor description per event; event bodies
   don't reach the user unless the agent echoes them. Skill now
   explicitly directs: one short chat line per event, paraphrase
   peer messages, stay silent on routine noise, surface state
   changes. This was the UX regression observed during the rollout
   — two tabs paired in #useideem but the user saw only a wall of
   identical "Monitor event" lines with no content.

Plus one troubleshooting bullet: after `airc canary` / `airc update`,
the running monitor still uses the old binary; teardown + rejoin
to pick up new code. Exactly what bit the authenticator-side Claude
on Monday demo morning.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New top-level section "Auto-scope — the default room" with:
    * The rule (git remote owner → parent-dir → #general)
    * Worked example with a generic ~/work/my-org/* + ~/work/cambriantech/* layout
    * "Scoping is the default, not a wall" subsection explaining airc list
      + airc join --room <other> + AIRC_NO_AUTO_ROOM=1 for cross-room hops
- Swap the old "The Magic" bullet from a verbose inline description
  to a short pitch + link into the new section.
- Update "Same gh account" setup to reference the auto-scope room
  rather than saying "hosts #general" (now only true when auto-scope
  falls through).
- Table row + skill descriptions + quick-start comments: rename
  "Auto-#general" to "Auto-scope" throughout.

Also scrub the skill file of PII from the original rollout (per
Joel): no joelteply/useideem references, no "Joel's Mac / Brian's
Linux box" phrasings. Generic my-org / your-username examples +
cambriantech where a concrete name helps (public, self-promoted).

Pairs with the auto-scope feature landed in #66; completes the
"make the docs match the behavior" pass that the rollout demo
surfaced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The mental-model table appears very early in the README (§"The
mental model: IRC..."), so a stale "airc join (auto-joins #general)"
row misleads first-time readers before they ever reach the
Auto-scope section. Replace with a one-line description that
matches the feature and anchors into #auto-scope--the-default-room
for the full worked example.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…larity

docs+skills(join): align with auto-scope, add narrate-events guidance, scrub PII
The 12-char truncation was producing awkward stubs in the auto-derived
agent name. `~/Development/ideem/authenticator` came back as
`authenticato-fd63` instead of `authenticator-fd63`; the missing
trailing 'r' was an immediate cognitive speed bump for anyone trying
to match agent names to repos in a busy multi-tab room.

Bump the cap to 20 chars so common repo names fit cleanly:
  authenticator        → authenticator-d1f4
  vHSM                 → vhsm-d1f4
  trusted-app-service  → trusted-app-service-993e
  merchant-browser-sim → merchant-browser-sim-XXXX
  airc                 → airc-d1f4

Also tighten the sanitization pipeline: collapse runs of dashes
(`cambriantech--airc-src` → `cambriantech-airc-src`), strip leading
and trailing dashes, and re-strip after the cut so a truncation that
falls on a dash doesn't leave a dangling tail. Pure cosmetic — safer
default, no behavior change beyond the cap.

Within an auto-scoped room, every peer is from the same org by
construction (see infer_repo_org from #66), so the org prefix would
just be redundant noise on every name. The repo basename + 4-char
hash is the right granularity for the room's audience.

Existing names persist in `<scope>/.airc/identity` — derive_name
fires on first-init, not on every connect. To pick up the new form
in an existing scope: `airc nick <new>` for in-place rename, or
`airc teardown --flush && airc join` for a fresh derive.

97/97 integration tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(derive_name): bump basename cap 12 → 20 so repo names fit
Adds structured agent persona on top of the bootstrap name from
derive_name. Five fields, all optional, all settable post-pair:

  pronouns      she/they/he/it for grammatical narration
  role          short hyphenated tag, e.g. device-link-orchestrator
  bio           one-line free-form, IRC-realname analog
  status        mutable "what I'm working on now" (Slack-like)
  integrations  {platform: handle} mappings (continuum, slack, …)

Stored under config.json's `identity` key (single-file scope) and
exchanged at pair-handshake time so peers cache each other's identity
locally and `airc whois` works without round-trips.

New commands:
  airc identity show
  airc identity set [--pronouns X] [--role Y] [--bio "…"] [--status "…"]
  airc identity link <platform> <handle>
  airc identity import <platform>:<id>      # continuum live, others stub
  airc identity push <platform>             # continuum live, others stub
  airc whois [<peer>]                       # self / host / paired peer / cross-peer-via-host
  airc kick <peer> [reason]                 # host-only IRC-style kick

Handshake protocol additions (backward compatible — missing fields
default to {}, so a v1 host pairing with a legacy joiner just gets
empty identity):
  - Joiner sends `identity` blob alongside ssh_pub/sign_pub
  - Host stores it in peers/<jname>.json under `identity`
  - Host returns own identity in response; joiner caches as host_identity
  - Cross-peer WHOIS reads remote peer file via single SSH cat

Skill updates (skills/join/SKILL.md):
  - New §2a "Identity bootstrap" — prompt agent to fill empty fields
    on every /join with persistent (non-nagging) re-prompts, skip
    via AIRC_NO_IDENTITY_PROMPT=1
  - §2b updated — auto-WHOIS on peer-join so context loads when a
    new peer joins the room

Test harness fix (incidental but load-bearing):
  cleanup_dirs was silently broken on macOS — `find /tmp -maxdepth 1`
  doesn't traverse the /tmp → /private/tmp symlink, so stale state from
  previous suite invocations leaked forward and caused flaky
  scenario_identity failures (saw "pronouns: they" survive into a
  fresh run). Resolve to canonical path before walking. Linux
  unaffected.

cmd_connect's host-mode + joiner-mode config writes converted from
heredoc to python json-merge so the `identity` block survives
re-pairs (teardown + airc connect), matching user expectation that
identity is sticky like nick.

Three new integration scenarios (29 new assertions, 126/126 total):
  scenario_identity   — local set/show/link/whois/persistence
  scenario_whois      — handshake exchange (host→joiner direction)
  scenario_kick       — host kicks joiner; joiner kick attempt refused

Deferred to follow-ups (not blocking this PR):
  - airc.ps1 parity for Windows (talking to green on continuum side)
  - airc ban (permanent block, complement to kick)
  - Live slack/telegram/discord import-push (continuum is the v1
    integration that actually works)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(identity): agent identity layer + cross-platform linking (closes #34)
- IRC-vs-airc table: add WHOIS / AWAY-style status / kick / USER-realname /
  cross-platform-identity rows so first-time readers see the IRC-parity
  story.
- "The Magic" bullet: new entry covering the bootstrap-prompt moment +
  cross-platform integrations.
- Core Commands: new "Identity" subsection listing show/set/link/
  import/push/whois/kick.
- New "Agent identity & WHOIS" section with the JSON shape, the field
  table, the bootstrap behavior, the handshake-time exchange story, the
  link-don't-duplicate pattern, and the kick semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
docs(readme): cover the new identity layer (post-#70)
…ad-bearing)

The Windows port has shipped (install.ps1, airc.ps1, install.sh Git
Bash compat all merged), and PR #70's identity layer has surfaced the
follow-up parity work for green's PS1 side directly via airc DM rather
than this file. The doc was useful as a handoff-tracker during the
two-tier port; it's now stale and clutters the repo root for fresh
clones.

Per Joel: "WINDOWS-PORT-STATUS.md can go now if it is now complete no
need for a doc sitting there."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…us-doc

chore: drop WINDOWS-PORT-STATUS.md (port shipped)
Copilot AI review requested due to automatic review settings April 25, 2026 12:37
chore: merge main → canary (resolve README conflict for #73 promote)
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Promotes canary → main by landing the new identity layer (handshake exchange + CLI), fixing derive_name truncation/sanitization, and updating docs/skills for IRC-parity + auto-scope guidance.

Changes:

  • Add identity management commands (airc identity ...), airc whois, and airc kick, including handshake-time identity exchange.
  • Improve derive_name (basename cap 12 → 20, tighter dash sanitization) and preserve identity when reconnecting by merging config.json.
  • Extend integration tests + update README and join skill docs; fix macOS /tmp cleanup in the test harness.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
airc Implements identity layer, whois/kick, handshake identity caching, and derive_name update.
test/integration.sh Fixes /tmp cleanup on macOS; adds new identity/whois/kick scenarios.
skills/join/SKILL.md Updates join skill for auto-scope defaults + identity bootstrap + event narration guidance.
README.md Documents auto-scope and the new identity/WHOIS/kick command set.
WINDOWS-PORT-STATUS.md Removes obsolete Windows port tracking document.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread airc Outdated

_identity_link() {
local platform="${1:-}" handle="${2:-}"
[ -z "$platform" ] && die "Usage: airc identity link <platform> <handle>"
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

airc identity link supports unlinking by omitting/blanking the handle, but the usage/error text still implies <handle> is required. Update the usage string (and the cmd_identity help line) to reflect that the handle is optional and that an empty handle unlinks, so users aren't misled.

Suggested change
[ -z "$platform" ] && die "Usage: airc identity link <platform> <handle>"
[ -z "$platform" ] && die "Usage: airc identity link <platform> [handle] (omit or pass empty handle to unlink)"

Copilot uses AI. Check for mistakes.
Comment thread airc Outdated
Comment on lines +2489 to +2503
# Pull peer's SSH pubkey out of authorized_keys (best-effort — pubkey
# lives in $PEERS_DIR/$target.pub if recorded; otherwise we leave the
# ssh key in place and rely on peer-file deletion to break the
# bookkeeping). Each step `|| true` since set -euo pipefail is active
# and benign no-match / missing-file cases are normal here.
local peer_pub_file="$PEERS_DIR/$target.pub"
if [ -f "$peer_pub_file" ] && [ -f "$HOME/.ssh/authorized_keys" ]; then
local peer_pub; peer_pub=$(cat "$peer_pub_file" 2>/dev/null || echo "")
if [ -n "$peer_pub" ]; then
# grep -v returns 1 when every line matches (or the file is empty);
# both are fine outcomes here, so eat the exit code.
grep -vF "$peer_pub" "$HOME/.ssh/authorized_keys" > "$HOME/.ssh/authorized_keys.tmp" 2>/dev/null || true
[ -f "$HOME/.ssh/authorized_keys.tmp" ] && mv "$HOME/.ssh/authorized_keys.tmp" "$HOME/.ssh/authorized_keys"
chmod 600 "$HOME/.ssh/authorized_keys" 2>/dev/null || true
fi
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd_kick attempts to remove the peer's SSH key from ~/.ssh/authorized_keys using $PEERS_DIR/$target.pub, but the host handshake writes peers/<name>.pub from sign_pub (the signing key), not ssh_pub. As a result, kick will usually leave the joiner's SSH key authorized, so the peer may still be able to SSH even after being “kicked”. Store the joiner’s ssh_pub in the peer record (e.g. in <peer>.json) and use that value for authorized_keys removal, and adjust the confirmation message accordingly.

Copilot uses AI. Check for mistakes.
Comment thread test/integration.sh
Comment on lines +1349 to +1352
# ── Peer file gone ──
[ ! -f /tmp/airc-it-k-h/state/peers/kjoiner.json ] \
&& pass "kicked peer's file removed" \
|| fail "peer file still present after kick"
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scenario_kick's header comment says kick removes the peer's pubkey from authorized_keys, but the scenario never asserts that the joiner’s SSH public key was actually removed from the host’s ~/.ssh/authorized_keys. Adding an explicit before/after grep for the joiner’s ssh_key.pub would both cover the advertised behavior and catch regressions in the key-removal implementation.

Copilot uses AI. Check for mistakes.
Comment thread airc
Comment on lines +2249 to +2255
ensure_init
local sub="${1:-show}"
shift 2>/dev/null || true
case "$sub" in
show|"") _identity_show ;;
set) _identity_set "$@" ;;
link) _identity_link "$@" ;;
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd_identity shifts two arguments after reading the subcommand (shift 2), which drops the first flag for set/link/import/push (e.g., airc identity set --pronouns they becomes args=["they"] and fails parsing). Change this to shift only the subcommand (or conditionally shift based on arg count) so flags are preserved.

Copilot uses AI. Check for mistakes.
Comment thread airc
Comment on lines +2387 to +2410
# Local peer file — flat layout: $PEERS_DIR/<peer>.json
local peer_file="$PEERS_DIR/$target.json"
if [ -f "$peer_file" ]; then
local blob; blob=$(python3 -c "
import json
p = json.load(open('$peer_file'))
print(json.dumps(p.get('identity', {}) or {}))
" 2>/dev/null)
local host; host=$(python3 -c "
import json
print(json.load(open('$peer_file')).get('host', ''))
" 2>/dev/null)
_whois_pretty "$target" "$blob" "$host"
return 0
fi

# Cross-peer via host (we're a joiner; query host's peer file remotely)
local host_target; host_target=$(get_config_val host_target "")
local host_airc_home; host_airc_home=$(get_config_val host_airc_home "")
if [ -n "$host_target" ] && [ -n "$host_airc_home" ]; then
local remote_blob
remote_blob=$(relay_ssh "$host_target" "cat $host_airc_home/peers/$target.json 2>/dev/null" 2>/dev/null || true)
if [ -n "$remote_blob" ]; then
local peer_id; peer_id=$(printf '%s' "$remote_blob" | python3 -c '
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd_whois uses the user-supplied target directly in local/remote file paths (e.g. peer_file="$PEERS_DIR/$target.json" and cat $host_airc_home/peers/$target.json). This allows path traversal (e.g. ../config) and, in the remote case, shell injection if target contains metacharacters. Validate target against the same safe charset as nicknames (e.g. ^[a-z0-9-]+$) and/or properly quote/escape the remote path before passing it to SSH.

Copilot uses AI. Check for mistakes.
Comment thread airc
Comment on lines +2472 to +2486
ensure_init
local target="${1:-}"
[ -z "$target" ] && die "Usage: airc kick <peer> [reason]"
shift || true
local reason="${*:-no reason given}"

# Joiner role check — kicking only makes sense as host.
local host_target; host_target=$(get_config_val host_target "")
if [ -n "$host_target" ]; then
die "kick: only the room host can kick. You are a joiner of $host_target — talk to the host."
fi

local peer_file="$PEERS_DIR/$target.json"
if [ ! -f "$peer_file" ]; then
die "kick: '$target' not in peers list (try: airc peers)"
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cmd_kick constructs peer_file="$PEERS_DIR/$target.json" from the unsanitized CLI argument. A value like ../config would match an existing file and then be deleted by rm -f, which is an easy footgun and a path traversal vulnerability. Reject peer names that don't match the allowed nickname format (e.g. ^[a-z0-9-]+$) before using them in filesystem paths.

Copilot uses AI. Check for mistakes.
joelteply and others added 2 commits April 25, 2026 07:51
Five concrete fixes from Copilot's review of #73 (one false positive
about a `shift 2>/dev/null` mis-read as `shift 2` left as-is):

1. **kick was removing the wrong pubkey** — `<peer>.pub` holds the
   signing key (sign_pub), not the SSH auth key. cmd_kick was reading
   that file and trying to grep it out of authorized_keys, which never
   matched. Result: kicked peers kept SSH access. Now host handshake
   stores `ssh_pub` in the peer JSON record; cmd_kick reads from
   there. Falls back to a clear "no SSH key recorded" warning for
   pre-#34 peers (whose handshake didn't carry ssh_pub).

2. **path traversal in cmd_whois / cmd_kick** — peer name was
   interpolated into local file paths AND remote SSH cat commands
   without sanitization. `airc whois ../config` would have been
   happily resolved against the joiner's host. New _validate_peer_name
   helper enforces `[a-z0-9-]+` (same charset cmd_rename already
   uses), no leading dash. Wired into both commands.

3. **kick test never asserted authorized_keys removal** — scenario
   covered the peer file removal but not the SSH key revocation,
   which is the actual security boundary. Now greps authorized_keys
   for the joiner's pubkey before/after kick. Would have caught (1)
   immediately.

4. **identity link usage string lied** — claimed `<handle>` was
   required, but the code actually unlinks when handle is omitted.
   Updated usage + help to reflect optional/blank handle = unlink.

5. **path-traversal regression test** — scenario_kick now also
   asserts that `airc whois ../config` and `airc kick ../config`
   both fail with "invalid peer name". Defense in depth.

130/130 integration tests pass (4 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Joel: airc is the chat substrate that came out of continuum
work, and the broader claim is that collaborative agentic systems are
the unlock. One italic line right under the title to set context for
fresh-clone readers before they hit the install instructions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joelteply and others added 4 commits April 25, 2026 07:55
fix: address Copilot review on #73 (kick auth bug + path traversal) + README tagline
Three changes:

1. **cmd_rename leading-dash regression** — the leading-dash check fired
   BEFORE sanitization, so input like `.foo` slipped past (the case
   check) and emerged as `-foo` after `[^a-z0-9-]/-/g` substitution. The
   resulting nick was unreachable to airc whois / airc kick (both
   reject leading-dash via _validate_peer_name). Fix: align cmd_rename's
   sanitization pipeline with derive_name's — collapse dash runs, strip
   leading and trailing dashes after sed AND after the cap. New test
   in scenario_identity asserts `airc nick .dottyname` produces a
   non-leading-dash result.

2. **scenario_kick guard improvement** — the authorized_keys
   assertions were guarded by `if [ -n "$kj_ssh_pub" ] && [ -f auth ]`,
   which silently skipped the entire kick-revocation check if either
   precondition was missing. Both should always be present here
   (init_identity always generates ssh_key.pub; pair handshake always
   creates authorized_keys). Now: each precondition is its own pass/
   fail, and a missing one short-circuits with cleanup_all + return so
   the kick assertion isn't silently elided.

3. **README title — pure shell** — add a hyphen-tagline to the H1 so
   the technical pitch (no compiled binary, no runtime install, just
   bash + PowerShell scripts) is the first thing fresh-clone readers
   see. Per Joel after pulling up the GitHub language breakdown:
   64% Shell, 35% PowerShell, 0.1% Batchfile.

133/133 integration tests pass (3 new — 2 from scenario_kick guard
expansion, 1 in scenario_identity for the dash strip).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per Copilot's nit on PR #76 + Joel's call: "pure shell" in the title
overstated since python3 is a system dep (used for inline JSON parsing
throughout airc). Pull the hyphen out of the H1 and fold the
shell-scripts-only claim into the existing descriptive tagline so it
lands as part of "what this is" rather than as a misleading title
suffix. Title is back to the plain project name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: PR #75 Copilot follow-up + 'pure shell' title hyphen
Copilot AI review requested due to automatic review settings April 25, 2026 13:16
joelteply and others added 2 commits April 25, 2026 08:17
Per Joel: the load-bearing claim isn't "no Python" (Python IS a system
dep, used for inline JSON parsing throughout airc). It's "no real
changes to your system as it is — you have/need gh and python anyway,
so the project isn't some dependency nightmare." The audience is devs
and tech enthusiasts who already have these.

So:
- Pull "shell scripts only (bash + PowerShell), no compiled binary"
  out of the italic tagline (where it overstated). Tagline is back to
  the cleaner thesis-only framing.
- Move the claim — explained — into the blockquote's closing line:
  "The whole thing is shell scripts ... the prereqs (git, gh, python)
  are things any developer's machine already has." That lands the
  point ("no system pollution") without a misleading title-level
  shorthand.
- While there: replace the stale "you're in #general" with "you're in
  your project's room" so the blockquote matches the auto-scope
  feature shipped in #66.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ou-have

docs(readme): leverage-what-you-have framing in the blockquote
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread airc
Comment on lines +2443 to +2444
local remote_blob
remote_blob=$(relay_ssh "$host_target" "cat $host_airc_home/peers/$target.json 2>/dev/null" 2>/dev/null || true)
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remote whois path builds an SSH command with an unquoted $host_airc_home (from config/handshake) embedded into the remote shell command. If the path contains spaces or shell metacharacters, this will fail; worse, a malicious/compromised host could inject additional shell syntax via host_airc_home. Consider robust quoting/escaping (or avoid the remote shell entirely by invoking a remote python/json reader with a safely-passed file path) before concatenating into an SSH command string.

Suggested change
local remote_blob
remote_blob=$(relay_ssh "$host_target" "cat $host_airc_home/peers/$target.json 2>/dev/null" 2>/dev/null || true)
local remote_blob remote_peer_file remote_peer_file_q
remote_peer_file="$host_airc_home/peers/$target.json"
remote_peer_file_q=$(python3 -c '
import shlex, sys
print(shlex.quote(sys.argv[1]))
' "$remote_peer_file" 2>/dev/null)
remote_blob=$(relay_ssh "$host_target" "cat -- $remote_peer_file_q 2>/dev/null" 2>/dev/null || true)

Copilot uses AI. Check for mistakes.
Comment thread airc
Comment on lines +2648 to +2651
ident[k] = v
ints = ident.setdefault("integrations", {})
ints["continuum"] = src.get("name", "")
json.dump(c, open(os.environ["CONFIG"], "w"), indent=2)
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the continuum import merge, ints["continuum"] is set to src.get("name", "") even when JSON parsing fails (src becomes {}), which overwrites any existing continuum link with an empty string. It would be safer to only update the integrations mapping when the imported persona name is non-empty (or fall back to the requested id), otherwise keep the existing link intact.

Copilot uses AI. Check for mistakes.
Comment thread test/integration.sh
Comment on lines +1254 to +1260
AIRC_HOME="$home/state" "$AIRC" nick ".dottyname" >/dev/null 2>&1 || true
local renamed; renamed=$(python3 -c "import json; print(json.load(open('$home/state/config.json')).get('name',''))" 2>/dev/null)
case "$renamed" in
-*) fail "airc nick produced leading-dash name '$renamed' — sanitization regression" ;;
"") fail "airc nick wrote empty name — sanitization regression" ;;
*) pass "airc nick strips leading dash post-sanitization (got '$renamed')" ;;
esac
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This nick sanitization regression check can false-pass because the airc nick invocation ignores failures (|| true). If the command errors for an unrelated reason, $renamed may remain the old valid name and the test still passes. Consider asserting airc nick exits 0 and/or that the stored name actually changed to the expected sanitized value before checking the leading-dash invariant.

Suggested change
AIRC_HOME="$home/state" "$AIRC" nick ".dottyname" >/dev/null 2>&1 || true
local renamed; renamed=$(python3 -c "import json; print(json.load(open('$home/state/config.json')).get('name',''))" 2>/dev/null)
case "$renamed" in
-*) fail "airc nick produced leading-dash name '$renamed' — sanitization regression" ;;
"") fail "airc nick wrote empty name — sanitization regression" ;;
*) pass "airc nick strips leading dash post-sanitization (got '$renamed')" ;;
esac
local renamed
if AIRC_HOME="$home/state" "$AIRC" nick ".dottyname" >/dev/null 2>&1; then
renamed=$(python3 -c "import json; print(json.load(open('$home/state/config.json')).get('name',''))" 2>/dev/null)
case "$renamed" in
-*) fail "airc nick produced leading-dash name '$renamed' — sanitization regression" ;;
"") fail "airc nick wrote empty name — sanitization regression" ;;
"dottyname") pass "airc nick strips leading dash post-sanitization (got '$renamed')" ;;
*) fail "airc nick wrote unexpected sanitized name '$renamed' (expected 'dottyname')" ;;
esac
else
fail "airc nick .dottyname failed unexpectedly"
fi

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment on lines 86 to +88
- **Open a new tab.** `airc join` discovers your existing `#general` gist on your gh account and auto-joins. **No string typed.**
- **Open a new machine.** Same gh account, same `airc join`, same auto-join. The mesh extends across the internet via gh.
- **`cd` into a git repo → land in the right room automatically.** `airc join` from any `useideem/*` checkout defaults to `#useideem`; any `cambriantech/*` checkout defaults to `#cambriantech`; any `joelteply/*` personal project defaults to `#joelteply`. The room name comes from the git remote's owner, so Joel's Mac and Brian's Linux box agree on the room without coordinating paths. Non-git dirs fall through to `#general` (the lobby). Override any time with `--room <name>` or `AIRC_NO_AUTO_ROOM=1`.
- **`cd` into a git repo → land in the right room automatically.** `airc join` with no flags defaults to a room named after the git remote's owner, so your work org's repos converge in one channel, your side projects converge in another, and you don't have to think about it. See **[Auto-scope — the default room](#auto-scope--the-default-room)** for the worked example. Non-git dirs fall through to `#general` (the lobby). Override any time with `--room <name>` or `AIRC_NO_AUTO_ROOM=1`, and `airc list` + `airc join --room <other>` lets any agent hop across rooms at will — scoping is the default, not a wall.
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bullet still claims airc join auto-joins #general, but the default behavior described immediately below is now auto-scope to the current repo org (with #general only as a fallback). Updating this line to match the new default (or explicitly qualify it as the non-git-dir fallback case) would prevent reader confusion.

Copilot uses AI. Check for mistakes.
Comment thread airc
Comment on lines +1613 to +1622
import json, os
try:
c = json.load(open(os.environ["CONFIG"]))
except Exception:
c = {}
c["name"] = os.environ["MY_NAME"]
c["host"] = os.environ["MY_HOST"]
c["host_target"] = os.environ["SSH_TARGET"]
c["created"] = os.environ["CREATED"]
json.dump(c, open(os.environ["CONFIG"], "w"), indent=2)
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config writes here use an in-place json.dump(..., open(CONFIG, "w")), which can transiently leave a partially-written config.json if another process reads mid-write (the integration test later adds a sleep to avoid flaking on half-written config). Consider switching to an atomic write (write to a temp file, fsync, then rename/replace) so readers either see the old or new full JSON.

Suggested change
import json, os
try:
c = json.load(open(os.environ["CONFIG"]))
except Exception:
c = {}
c["name"] = os.environ["MY_NAME"]
c["host"] = os.environ["MY_HOST"]
c["host_target"] = os.environ["SSH_TARGET"]
c["created"] = os.environ["CREATED"]
json.dump(c, open(os.environ["CONFIG"], "w"), indent=2)
import json, os, tempfile
config_path = os.environ["CONFIG"]
try:
c = json.load(open(config_path))
except Exception:
c = {}
c["name"] = os.environ["MY_NAME"]
c["host"] = os.environ["MY_HOST"]
c["host_target"] = os.environ["SSH_TARGET"]
c["created"] = os.environ["CREATED"]
config_dir = os.path.dirname(config_path) or "."
fd, tmp_path = tempfile.mkstemp(dir=config_dir, prefix=".config.", suffix=".tmp")
try:
with os.fdopen(fd, "w") as f:
json.dump(c, f, indent=2)
f.flush()
os.fsync(f.fileno())
os.replace(tmp_path, config_path)
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)

Copilot uses AI. Check for mistakes.
Comment thread airc
Comment on lines +1872 to +1876
# Host mode: clear any leftover host_target/host_name from a prior
# joiner run in this scope (avoid mis-reading ourselves as a joiner).
for k in ("host_target", "host_name", "host_port", "host_airc_home", "host_ssh_pub", "host_identity"):
c.pop(k, None)
json.dump(c, open(os.environ["CONFIG"], "w"), indent=2)
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same atomicity concern as the joiner branch: this merges into config.json with an in-place write. Since other subprocesses (monitor/identity/whois) can read config.json while airc connect is running, a non-atomic write risks corrupted/empty reads. Prefer an atomic write pattern (temp file + replace) to make config updates crash-safe and race-safe.

Copilot uses AI. Check for mistakes.
@joelteply joelteply merged commit 489a590 into main Apr 25, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants