fix(gateway,skills): persist pairing tokens and skills with atomic replace - #98
Conversation
Write skill markdown via unique temp+fsync+rename so fopen("w") cannot
wipe an existing skill on ENOSPC or crash.
Refs: #77
There was a problem hiding this comment.
Summary
ShellClaw is a C99 edge agent (Jetson / Raspberry Pi) with a ReAct loop, gateway pairing auth, and markdown skills under ~/.shellclaw/skills/. This PR delivers the persist-integrity fixes for those two stores: pairing tokens no longer die to O_TRUNC, and skill updates no longer die to fopen("w") truncate-before-write. auth_pair also fails closed when bearer RNG fails. The persist path matches write_file_atomic in src/tools/file.c (unique mkstemp, fsync, rename, unlink on failure). I ran CI=true gcc -Werror test_auth and test_skill (both pass, linked -lm) and the same two binaries under ASan+UBSan (both pass). GitHub static, test, release was still pending at review time.
Must Fix
None.
Should Fix
-
Third copy of the atomic-write helper.
write_tokens_atomic(src/gateway/auth.c:199) andwrite_skill_atomic(src/core/skill.c:405) are the same function aswrite_file_atomic. The next durability change (directoryfsync,EINTRretry) will have to land in three places. Extract a shared helper in a follow-up. This PR is right not to mix that into the bugfix. -
/pairtreats persist and RNG failure as a bad pairing code.handle_pairinsrc/gateway/routes.c(unchanged here) maps everyauth_pairnon-zero return to HTTP 400 "Invalid pairing code" andauth_pair_record_failure(5 strikes, 300s). RNG failure is newly a-1, which is the correct fail-closed behavior, but a valid code plus a full disk or dead urandom now burns lockout. Distinct return codes for invalid code vs operational failure would avoid that. -
Skill files become mode 0600.
mkstempcreates 0600.file.cthenfchmods 0644. This copy does not, soskill_create/skill_updatestop usingfopen("w")umask (typically 0644). Fine for a single-user agent. If anything else readsskills/, it loses group/other read after the first save.
Nice to Have
- Explicit
fchmod(fd, 0600)plus ast_mode & 0077check onauth_tokens.json, matchingmanifest_keys.c. glibcmkstempis already 0600; this would make the credential mode explicit rather than an implicit libc property. fsyncthe parent directory afterrenameif the crash claim should cover a power loss before the dirent is journaled.file.cdoes not do this either.src/core/skill.cis 532 lines after this change (was under 500). Not a merge gate; the in-tree line cap that is actually enforced is the 1000-lineconfig.crule.- Assert
auth_tokens.jsonis 0600 after a successful pair.
Positive Highlights
- Unique
.sc-auth-XXXXXX/.sc-skill-XXXXXXsidecars avoid the predictablepath.tmpsibling clobber left open on #94. - Skill loader already skips
d_name[0] == '.', so in-flight temps are not injected into the system prompt. - Pairing code is cleared only after a successful persist (
src/gateway/auth.c:288-289). The urandom test checks leftovertoken_out, no tokens file, and a retry with the same code. RLIMIT_FSIZEtests fail the write, assert the previous content still validates, then succeed after the limit is restored. That is the actual bug, not a coverage bump.- CHANGELOG lists #71/#92 under Security and #77 under Fixed. Two conventional commits, scoped away from file/cron/sandbox.
Sent by Cursor Automation: Adrianno鈥檚 personal code review


Summary
development(not a cherry-pick of the Cursor drafts).auth_pairwritesauth_tokens.jsonvia unique temp (mkstemp)+fsync+rename soO_TRUNCcannot wipe existing tokens, and fails closed when bearer RNG fails (pairing code kept).skill_create/skill_updatepersist markdown the same way sofopen("w")cannot wipe an existing skill.Supersedes #71, #92, #77.
Does not mix file/camera, cron, shell, asap, config, or sandbox.
Test plan
make test_auth/./build/test_authmake test_skill/./build/test_skillCI=true make testmake staticMade with Cursor