Skip to content

Teach the remote shim the Artifactory's five verbs - #282

Merged
scgopi merged 2 commits into
mainfrom
feat/281-artifactory-remote-shim
Sep 4, 2026
Merged

scgopi merged 2 commits into
mainfrom
feat/281-artifactory-remote-shim

Conversation

@scgopi

@scgopi scgopi commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Closes #281.

A loop on an ssh:// or Codespaces host is handed the briefing's full "The Artifactory — notes for whoever comes next" section, told to check the board at the start of every pass, and then refused by its CLI on all five verbs. Its own node sends are mirrored onto that board by the daemon, so a remote loop was not absent from the Artifactory — it was write-only and blind: visible to every peer, unable to read a word anyone said back.

Shim-only, as #281 established

No DaemonProtocol, RemoteSocketForwarder, GraphStore or SessionBriefing change. read and list send no command at all — openProject's graphChanged snapshot already carries the whole board and every node's lastArtifactoryRead.

Verb How it works
post one artifactoryPost frame; prints posted #N (topic) from the returned graph
sync artifactorySync, rendered from the snapshot taken before the cursor moves; --headlines --full --mark --json, same precedence as the Swift CLI
read snapshot only, no command
list snapshot only; --search --json
watch one artifactoryWatch frame; --topic --off

sync and watch refuse a human shell up front in the Swift CLI's own wording, before dialling, so nothing is sent and nothing needs undoing.

Plus the sixth gap from the issue: the shim's render(graph) omitted the board status line the local CLI appends (renderArtifactoryStatusLine), so graphcode status hid the board from a remote loop even with the snapshot in hand. Fixed here.

The risk, and what answers it

This adds a second copy of the renderer — Python, inside the shim — and cliShimStamp is content-derived, so a defective shim propagates to every remote host on the next ensure and silently breaks node send and memo for every loop already running there.

What makes that safe is RemoteCLIShimTests asserting the two byte-equal, to the standard PR #275 set in CodexReadinessGateTests: a real socket, the production cliShimSource executed as delivered, and every expected value computed by calling GraphcodeCommand's own renderer — never a string written by hand. 13 new tests, 18 in the suite.

The parity test earned its keep twice, on drift I would not have found by reading:

  1. The reference-date offset is invisible in the stamp. 978307200 seconds is exactly 11323 days, which is exactly 31 years across the fixture's eight leap days — so a shim that dropped the offset renders an identical MMM d, HH:mm (Aug 23, 18:46 either way). The stamp alone can never catch it. The year is pinned through --json's ISO-8601 instead, where the mistake is unmissable.
  2. Swift's JSONEncoder escapes / as \/; json.dumps does not. The first fixture had no slash in any body, so the test passed while --json was genuinely wrong. Caught by the live check below, then fixed and pushed back into the fixture, which now carries a path and a URL. Reverting the fix fails the test — verified.

Also pinned: the 12-post / 4096-byte triage boundary on both sides and both directions; the identity refusals; that read/list send nothing past the open; and that artifactory no longer reaches the "Mac-only" refusal.

Verification

Not verified: an end-to-end run on a real remote host or Codespace. I had none available. Everything below ran locally against the same daemon socket the ssh forward carries, and the byte-equal parity is what this PR stands on.

Beyond the suite, the delivered shim source was extracted and run against the live daemon socket on the real project board (136 posts), diffed against the real Swift CLI:

Command Result
artifactory list <path> ✅ byte-identical
artifactory list --json ✅ byte-identical (after the \/ fix — this is where it was found)
artifactory list --search release / --search 0.1.59 / no match ✅ byte-identical
artifactory read <id> (present, and #999 absent) ✅ byte-identical, same exit code
status <path> board line ✅ identical (artifactory: 131 posts)

status still differs from the local CLI in the pre-existing ways — the shim renders no edges block and no attention/exit-code suffixes. Out of scope here; the board line itself matches.

Gate: 1573 tests pass, swiftlint 0 errors, swift format --strict clean, all three schemes (graphcode, graphcode-cli, graphcoded) build.

Independent review

Reviewed by a separate loop (ReviewPR282, OpenCode backend) against b248f1a, which re-ran the gate in its own worktree and confirmed 1573 tests pass, swiftlint 0 errors, swift format --strict clean, all three schemes building. Verdict: approve with notes, no blocking defects.

It verified the parity harness runs the production cliShimSource under python3 against a real AF_UNIX socket serving production graphChanged frames, with every expected value produced by the real GraphcodeCommand renderer — no hand-written renderer literals. It independently re-ran the live diff against the installed CLI on the real board, covering 63 non-ASCII bodies including a CJK post, and confirmed the \/ escaping match.

One divergence found, currently unobservable: the shim trims a note with Python .strip() (all Unicode whitespace) where Swift uses .whitespaces (space and tab only). It cannot be observed today because the daemon re-trims with .whitespacesAndNewlines on arrival (GraphStore.swift:1594) — but it is a latent trap if that store-side trim ever changes.

Recommended post-merge check: stamp propagation to a real host on first ensure, and live remote sync/watch attribution via ZMX_SESSION, remain unproven here — both are pre-existing machinery rather than shim logic. The first remote ensure after this merges should be watched once.

Known limits, deliberately not fixed

🤖 Generated with Claude Code

https://claude.ai/code/session_01AoU8LMupkqeoemNSob7VuM

scgopi and others added 2 commits September 4, 2026 11:11
A loop on an ssh:// or Codespaces host is handed the briefing's full
"notes for whoever comes next" section, told to check the board at the
start of every pass, and then refused by its CLI on post, sync, read,
list and watch: the Python shim dispatches seven verbs and artifactory
is not one of them. Its own node sends are mirrored onto that board by
the daemon, so a remote loop was not absent from the Artifactory, it was
write-only and blind -- visible to every peer, unable to read a word
anyone said back.

The refusal was entirely client-side. GraphCommand carries no host
discrimination, GraphStore dispatches the three board commands
unconditionally, and ssh -N -R forwards bytes without parsing frames, so
nothing in DaemonProtocol, RemoteSocketForwarder or GraphStore changes
here. read and list need no command at all: openProject's graphChanged
snapshot already carries the whole board and every node's cursor.

status gains the board line too. renderArtifactoryStatusLine has been on
the local CLI's render since the board shipped, and the shim's render
never had it -- so the cheap "is there mail" check the briefing sends
every loop to before claiming work was silently absent remotely, with
the snapshot it needed already in hand.

The renderer is now duplicated, Swift on the Mac and Python on the
remote host, and cliShimStamp is content-derived: a defective shim
propagates to every remote host on the next ensure and breaks node send
and memo for every loop already running there. What makes that safe is
RemoteCLIShimTests asserting the two byte-equal -- real session, real
shim source, production renderer, never a constructed string. It earned
its keep twice: the reference-date offset is exactly 11323 days, which
is exactly 31 years across the fixture's leap days, so a dropped offset
renders an identical MMM d, HH:mm stamp and the year had to be pinned
through ISO-8601 instead; and Swift's JSONEncoder escapes / as \/ where
json.dumps does not, which the fixture now carries a path and a URL to
catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoU8LMupkqeoemNSob7VuM
Five findings from review of the byte-equal port, four of them real
divergences the first fixture could not reach.

`--search` compared code points where Swift's String.contains compares
canonically, so a body carrying decomposed text -- anything sourced from
a macOS path routinely does -- was findable from the Mac and invisible
from the remote host. That is the board answering that mail does not
exist. Both sides now fold through NFC.

`renderHeadline`'s budget counted code points where Swift counts
extended grapheme clusters. The first commit documented this as a
character either way; measured, it cuts a body of decomposed accents at
15 characters against the Mac's 30, and can land between a base
character and its combining mark or inside a ZWJ sequence, ending a
remote headline on a mangled glyph. UAX #29 reduced to the joins that
actually reach a note -- combining marks, ZWJ, variation selectors,
skin-tone modifiers, flags -- now backs the cut, with every one of them
driven through both renderers.

A token starting with -- was accepted as the project path, so
`artifactory list --json` dialled the daemon and asked it to open a
project named --json instead of refusing up front the way parseArtifactory
does. Now refused before any dial.

A post's topic was tested for truthiness rather than presence, so an
empty topic rendered without the `()` Swift's Optional map produces. The
daemon refuses an empty topic today, which is exactly why the renderer
should not carry a second opinion about it.

Also: the deliveryFragment doc comment's stranded-receipt argument cited
a 14.6 KB shim; it is 31 KB now. The argument holds, the figure did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoU8LMupkqeoemNSob7VuM
@scgopi

scgopi commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

Review round 1 applied

All five findings addressed, each pinned by a test that fails when the fix is reverted (verified — reverting all four code fixes fails 16 assertions across 3 tests).

# Finding Fix
1 --search normalization-sensitive both needle and haystack fold through unicodedata.normalize("NFC", …) before the substring test
2 headline cut counts code points UAX #29 reduced to the joins that reach a note — combining marks, ZWJ, variation selectors, skin-tone modifiers, regional-indicator flags
3 a --flag accepted as the project path refused before the dial, matching parseArtifactory's take
4 empty-string topic rendered without () is not None, matching Swift's Optional map
5 doc comment said 14.6 KB shim now 31 KB

On #2 in particular — the review was right that the PR undersold it, and it turned out to be fixable rather than merely documentable. headlinesCutAtTheSameGraphemeClusterTheMacDoes drives six bodies through both renderers and asserts byte equality: 70 decomposed a+U+0301, 20 ZWJ families, skin-tone + flag + keycap sequences, a mixed body whose cut lands mid-sequence, astral non-joining, and plain ASCII. All byte-equal. The remaining approximation is Hangul jamo composition, which no note has ever contained.

On #4: agreed it is unreachable through the daemon today. Fixed anyway for the reason the finding gives — the renderer should not carry a second opinion about a rule that lives in GraphStore.

Gate re-run: 1573 tests pass (3 new), swiftlint 0 errors, swift-format strict clean, all three schemes build. Live diff against the real CLI on the now-132-post board re-confirmed byte-identical for list, list --json, --search, read, and the status board line.

Still unverified, unchanged from the PR body: no end-to-end run on a real remote host or Codespace — I have none available.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AoU8LMupkqeoemNSob7VuM

@scgopi
scgopi merged commit 828f2cc into main Sep 4, 2026
1 check 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.

Artifactory is unreachable from remote/Codespaces loops: the briefing teaches five verbs the shim refuses

1 participant