45ca7519f2cd
let koh remote replace or repair the setting on a terminal, refuse credentials others can read, and document the hand-written config files (3.0.58)
- time
- 2026-09-26 02:02 UTC · 2 weeks ago
- machine
- koh2
- agent
- claude-opus-5-5
- session
- remote setup
- parent
- 6287b5567dc3
↓ koh-45ca7519.tar.gzatlas at this save
files changed5 files
VERSION+1 −1src/cli/cmd_remote.v+18 −3src/machine/settings.v+7 −1tests/cli_test.v+43 −0v.mod+1 −1src/cli/cmd_remote.v+18 −3
@@ -43,7 +43,14 @@4343 }4444 return setup_wizard()4545 }46- settings := machine.load_remote() or { return refuse(args.json(), err.msg()) }46+ settings := machine.load_remote() or {47+ // Otherwise the message to run "koh remote" again would loop.48+ if !args.json() && interactive() {49+ warn(err.msg())50+ return setup_wizard()51+ }52+ return refuse(args.json(), err.msg())53+ }4754 if args.json() {4855 emit_json(json2.encode(RemoteJson{4956 configured: true@@ -53,8 +60,16 @@5360 return 05461 }5562 ok('remote: ${describe(settings)}')56- note('run "koh remote folder <path>" or "koh remote s3 <endpoint> <bucket> <region>" to replace it')57- return 063+ if !interactive() {64+ note('run "koh remote folder <path>" or "koh remote s3 <endpoint> <bucket> <region>" to replace it')65+ return 066+ }67+ print(' replace it? [y/N] ')68+ if os.get_line().trim_right('\r') != 'y' {69+ return 070+ }71+ // The old remote stays until the new one passes its test.72+ return setup_wizard()5873 }5974 // A non-interactive form, still tested before it is saved.6075 return set_remote(args)src/machine/settings.v+7 −1
@@ -121,8 +121,14 @@121121 }122122 123123 pub fn load_credentials() !Credentials {124- text := os.read_file(config_path(credentials_file)) or {124+ path := config_path(credentials_file)125+ text := os.read_file(path) or {125126 return error('this machine has no S3 keys yet — run "koh remote"')127+ }128+ // Like ssh with a private key: a hand-written file others can read is129+ // refused rather than used.130+ if os.stat(path)!.mode & 0o077 != 0 {131+ return error('${path} can be read by others — run "chmod 600 ${path}"')126132 }127133 values := parse_config(text)128134 credentials := Credentials{tests/cli_test.v+43 −0
@@ -657,6 +657,49 @@657657 assert run_home(dir, home, 'remote', '--json').out.contains('"configured":false')658658 }659659 660+// The files under ~/.config/koh can be written by hand instead of by661+// `koh remote`, and are then read the same way.662+fn test_remote_written_by_hand() {663+ dir := temp_repo('remote-by-hand')664+ home := temp_repo('remote-by-hand-home')665+ defer {666+ os.rmdir_all(dir) or {}667+ os.rmdir_all(home) or {}668+ }669+ config := os.join_path(home, '.config', 'koh')670+ os.mkdir_all(config) or { panic(err) }671+ target := os.join_path(home, 'remote-store')672+ os.mkdir_all(target) or { panic(err) }673+ os.write_file(os.join_path(config, 'remote'), 'kind = folder\npath = ${target}\n') or {674+ panic(err)675+ }676+ assert run_home(dir, home, 'init').code == 0677+ os.write_file(os.join_path(dir, 'a.txt'), 'a\n') or { panic(err) }678+ assert run_home(dir, home, 'save', 'first').code == 0679+ offered := run_home(dir, home, 'offer', '--json')680+ assert offered.code == 0681+ assert offered.out.contains('"head"')682+683+ // Keys anyone else can read are refused before they are ever sent.684+ os.write_file(os.join_path(config, 'remote'), 'kind=s3\nendpoint=http://127.0.0.1:1\nbucket=b\nregion=r\n') or {685+ panic(err)686+ }687+ credentials := os.join_path(config, 'credentials')688+ os.write_file(credentials, 'access_key=AK\nsecret_key=SK\n') or { panic(err) }689+ os.chmod(credentials, 0o644) or { panic(err) }690+ loose := run_home(dir, home, 'offer')691+ assert loose.code == 1692+ assert loose.err.contains('chmod 600')693+694+ // A setting koh cannot read is replaced by setting the remote again.695+ os.write_file(os.join_path(config, 'remote'), 'kind=ftp\n') or { panic(err) }696+ unreadable := run_home(dir, home, 'offer')697+ assert unreadable.code == 1698+ assert unreadable.err.contains('not readable')699+ assert run_home(dir, home, 'remote', 'folder', target).code == 0700+ assert run_home(dir, home, 'offer').code == 0701+}702+660703 // A clone belongs to the repo it was stolen from, whatever its folder is661704 // called later: renaming it must not start a second repo on the remote.662705 fn test_a_renamed_clone_keeps_offering_to_its_repo() {v.mod+1 −1
@@ -1,6 +1,6 @@11 Module {22 name: 'koh3'3- version: '3.0.57'3+ version: '3.0.58'44 description: 'One SQLite file per repo, one line of history.'55 license: 'MIT'66 }