Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# The macOS Cocoa key-bindings dict is carried as a readable old-style
# (NeXT/ASCII) text property list, never a binary plist. Mark it `text` so git
# treats it as text (line-ending normalisation, textual diffs) and an editor
# saving it as bplist00 binary shows up as a reviewable change rather than an
# opaque blob. ferry additionally refuses a bplist00 source at apply time.
keybindings/DefaultKeyBinding.dict text
8 changes: 8 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ paths = [
'''internal/plugin/zsh/analyze_test\.go$''',
# A fake AWS key fixture in the untrusted-git-transport tests.
'''cmd/sync_round3_test\.go$''',
# v0.7.0 config-plugin secret tests: credential-shaped fixtures that exercise
# the git/tmux/npm token recognisers and prove a literal token never reaches
# the shared repo. Fake tokens only; every non-test file stays fully scanned.
'''internal/gitconfig/secret_test\.go$''',
'''internal/tmux/secret_test\.go$''',
'''evals/git_test\.go$''',
'''evals/tmux_test\.go$''',
'''evals/npmrc_test\.go$''',
]
6 changes: 6 additions & 0 deletions .work/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ a `docs/decisions/` ADR when it shapes architecture or is expensive to reverse.
2026-07-06 Record placement (A1): DROP the abcd 7-folder Record; keep docs/{decisions,plans,research} only. No development/ tree, no scaffoldDocsDirs extension. Rejected: docs/ baseline w/ 4 new folders, top-level development/.
2026-07-06 CLI-ref (A5): use cobra/doc GenMarkdownTree; accept go-md2man+blackfriday as indirect requires, isolated in tools/gendocs (never linked into ferry binary). Rejected: hand-rolled walker.
2026-07-06 Extras: add root SECURITY.md. Rejected: seed Record content, extend scaffold target repos to new Record dirs.
- 2026-07-07 v0.7.0 Phase B tmux: carried as an include-sidecar dotfile on the EXISTING dotfiles FileDomain (declare `.tmux.conf`; `dotfilesFileDomain.Overlay("tmux.conf")` → OverlayIncludeSidecar) — no fn-5 plugin-owned dispatch needed. The include-directive SYNTAX is generalised to a per-bare-name `directiveSpec` (cmd/directive.go: marker + render + namesFile), zsh kept BYTE-IDENTICAL. The two-strip contract stays TWO distinct functions: cmd `stripSourceDirective` (file-keyed, spec-driven) and internal/dotfile `isFerryOverlayInclude` (shape-keyed) — the latter extended to also recognise `source-file -q ~/` so the empty-over-substantial overlay-bypass guard still strips ferry's tmux boilerplate. Overlay dir mapping centralised in `overlayDomainDir` (zsh→zsh, tmux.conf→tmux). tmux secret recogniser lives in NEW internal/tmux (SecretSpans: column span of a quoted `set -g @token '…'` value, gated by IsNonPlaceholderSecret + ScanText High), CALLING secret.SwapColumns — never teaching internal/secret tmux syntax. Capture routes tmux through the span-store path (spanRoutable/captureSpanExtractor) instead of whole-file captureBlocked. DECISION: a tmux secret NOT in a `set -g @token` shape (e.g. a stray PEM pasted into tmux.conf) is refused READ-ONLY rather than whole-file-stored — safe (never leaks) but the user handles it out of band. Rejected: falling back to line-grained FlaggedSpans for tmux (would clobber the `set` prefix on an @token line).
- 2026-07-07 v0.7.0 Phase B iTerm2: TWO artefacts. (A) Dynamic Profiles = new termcfg-shaped FileDomain `iterm2-profiles` (internal/iterm2profiles) fanning `iterm2/DynamicProfiles/*.json` → `~/Library/Application Support/iTerm2/DynamicProfiles/`; Captures()=false, repo-authoritative, frozen GUID (bytes copied verbatim, no capture pass so naturally satisfied). Per-file overlay `local/iterm2-profiles/<rel>` wins AND a local-only file deploys as a machine-only child profile (union walk, unlike emacs's override-only). Validation: pure-Go json.Valid on every platform PLUS darwin `plutil -convert xml1 -o /dev/null` — NOT `plutil -lint`, which guesses the old-style parser for `{`-leading content and REJECTS valid JSON (verified). This is why iterm2profiles keeps its OWN linter rather than reusing keybindings' (whose old-style dict genuinely needs -lint). (B) Global plist = D4 flip: NewITerm2(exportBlob, runner, ProcessController) imports via `defaults import` like Apple Terminal, RETIRING PrefsCustomFolder/LoadPrefsFromCustomFolder + the rendered-staging folder. Running-guard: ExecProcessController.Running() via `pgrep -x iTerm2` — apply REFUSES (ErrITerm2Running, clean skip, no rollback) when running because iTerm2 rewrites the domain on quit; after a not-running import, best-effort `killall cfprefsd`. Capture/status filter via terminal.FilterAllowlist (encoding/xml top-level-dict allowlist, sorted+whitespace-stripped → deterministic+idempotent) REPLACING StripITerm2ControlKeys — allowlist means volatile keys (NoSync*/geometry/retired pointer) can never reach the repo. KNOWN: `defaults import` REPLACES the whole domain, so a non-allowlisted global key is reset on apply (volatile ones regenerate; documented "extend the allowlist"). KNOWN wart: an apply that skips-because-running still registers+backups the resource (running-check is apply-time inside terminal.Apply), so prefApplied flips true — harmless (baseline = current state). Rejected: checking running at build/plan time (would make `diff` shell out to pgrep and vary preview by whether iTerm2 runs).
- 2026-07-07 v0.7.0 Phase B git (FLAGSHIP): `~/.gitconfig` rides the EXISTING dotfiles FileDomain as an include-sidecar dotfile (declare `.gitconfig`; `dotfilesFileDomain.Overlay("gitconfig")` → OverlayIncludeSidecar). NEW multi-line `gitDirective` in cmd/directive.go: `directiveSpec` gained an optional `header` field so the file-keyed strip drops git's TWO-line `[include]` block (header + `path=~/…`) — zsh/tmux leave header "" and stay BYTE-IDENTICAL. Shape-keyed half: internal/dotfile `stripFerryOverlayDirective` gained a git 2-line branch (`isFerryGitIncludePath`). Identity firewall = internal/gitconfig.SharedContent (hand-written byte-faithful git-INI parser proven Reassemble(Parse(x))==x by fuzz): drops user.email/name/signingkey, gpg.program, credential.helper, and whole `[includeIf …]` blocks, dropping now-empty identity sections; NO-OP (byte-for-byte) on a clean config so the round-trip stays byte-stable. Enforced on BOTH deploy-compose (planDotfiles include branch + effectiveZshShared) AND shared-capture write (captureOne RouteShared) via cmd sharedGitTransform — defense in depth: even a mis-committed identity key never reaches shared. signingkey is identity-NOT-secret (plaintext local, never the secret store). Secrets: internal/gitconfig.SecretSpans (column-grained token isolation for url.*.insteadOf URL userinfo + http.extraHeader Bearer/Basic), CALLING secret.SwapColumns/ScanText/IsNonPlaceholderSecret — never teaching internal/secret git syntax; wired via captureSpanExtractor/spanRoutable exactly like tmux. credential.helper=store WARNED (gitCredentialHelperWarnings) + stripped (it is identity); ~/.git-credentials never enumerated/read. Rejected: a separate git FileDomain (freeze fixture's toy shape) — the task chose the include-sidecar path so git reuses the whole dotfiles plan/capture/secret machinery.
- 2026-07-07 git plugin security review = APPROVE-WITH-NOTES. Fixed F1 (LOW/MED, real leak of a DECLARED identity key): an unclosed `[user` header made the following `email=` a section-less Junk line that SharedContent kept → identity in shared. Fix: parseSectionHeader now parses leniently WITHOUT a closing `]` (git rejects such a file, but ferry must still firewall it) so `[user` classifies as the user section and its keys drop; a bare `[]` resets section context to "" so a following key is never mis-attributed. Regression tests added (TestSharedContentMalformedHeaderNoLeak + round-trip-unaffected). Fixed F4 (quoted `helper="store"` missed the warning — valueFirstToken now trims quotes) and F3 (comment mismatch on per-host credential.helper). FLAGGED-NOT-FIXED F2: `credential.<host>.username` reaches shared — it is account identity but NOT in the signed-off 5-key identity set (plan §3.1: user.email/name/signingkey, gpg.program, credential.helper); leaving the declared contract unchanged rather than silently expanding scope. Owner should decide whether to add credential.username.
- 2026-07-07 git plugin ruthless-review: fixed 2 STOP-class identity leaks in the git-INI parser grammar. F1 INLINE assignment: git parses `[user] email = x` and `[user]email=x` as header+assignment; parser now SPLITS such a physical line into a Section Line + a KeyValue Line (byte-exact across the two Raw fields, so Reassemble(Parse(x))==x still holds), so the inline identity key is classified and dropped, and CredentialHelperStore catches inline `[credential] helper = store`. F2 backslash VALUE-CONTINUATION: git joins a value across lines ending in an ODD run of `\`; parser now tracks continuation state and marks the continuation physical line as a KeyValue INHERITING the owning key (dropped with it) instead of a fresh non-identity key. F3 (minor): extraHeaderRe token group `([^\s"']+)` stops before a trailing quote (no unbalanced-quote placeholder). F4 (cleanup): deleted dead test-only LocalIdentityContent; unexported IsIdentityKey→isIdentityKey (wired internally in identityDropSet). New tests: TestSharedContentInlineHeaderNoLeak, TestCredentialHelperStoreInline, TestSharedContentBackslashContinuationNoLeak (unit), eval TestGit_InlineAndContinuationIdentityStrippedOnDeploy; round-trip fuzz corpus gained inline+continuation seeds. FLAGGED-STILL-OPEN from prior security review: credential.<host>.username (F2 there) still reaches shared — owner decision, outside the signed-off 5-key set.
- 2026-07-08 git plugin: owner APPROVED adding credential.username to the identity firewall (resolves the earlier flagged F2). Added "credential.username" to identityKeys in internal/gitconfig/analyze.go — FullKey collapses the subsection, so it drops bare `[credential] username` AND per-host `[credential "https://host"] username` on both deploy (SharedContent) and shared-capture, kept plaintext-local (NOT secret-store-routed; it is account identity, not a secret). Tests: TestSharedContentDropsCredentialUsername (unit, canonical+inline+per-host), extended eval TestGit_IdentityNeverShared to seed a per-host username and assert it reaches only ~/.gitconfig.local. Docs: CHANGELOG [0.7.0] git bullet + docs/reference/configuration.md identity list updated. Identity set is now the SIX keys: user.email/name/signingkey, gpg.program, credential.helper, credential.username (+ all [includeIf] blocks).
Loading
Loading