Make the outbound dir writable by non-root consumers (0.3.0:5) - #11
Conversation
This container runs as root, so `.simplex/outbound` was created root-owned with 0755. A consumer stages outgoing files as its own uid, though — the openclaw package now runs OpenClaw as `node` (uid 1000) — so every send failed with EACCES. Receiving was unaffected: inbound is mounted read-only and simplex-chat writes received files world-readable. The bridge can't know a consumer's uid, and more than one consumer may mount the same dir, so chowning to a guess (node:node) would just move the breakage. Widen the mode instead: create `.simplex/outbound` 1777 — world-writable with the sticky bit, as on /tmp — so any uid can stage files while each consumer can only remove the files it staged. Mode-only, so it holds regardless of how uids are mapped into containers, and re-applied on every start so existing installs heal on upgrade with no migration. Reach is unchanged: only packages that declare this dependency and mount the subpath can see the dir at all. Documents the permission model in the file-exchange contract (README + the mainMounts contract comment) — it was previously silent on uid/ownership, which is why the consumer side hit this at runtime rather than at review.
…source mkdir's mode is umask-masked, and on a fresh install this is now the code that creates `.simplex` — so it was landing at 0777 & ~umask on the dir holding the profile database and keys. Pin it to 0700. Only root inside this container ever traverses it: a dependency mount is resolved by StartOS when it sets the mount up, and the consumer then reaches the subpath through a path in its own namespace. An existing `.simplex` is left alone — recursive mkdir doesn't re-mode a dir that's already there. The rationale for the mode was written out three times in the source, with main.ts and the OUTBOUND_MODE doc comment both narrating the openclaw incident that the commit message and PR body already carry — and that the next consumer will make wrong. Keep the mechanism in main.ts, the contract on OUTBOUND_MODE, and the reasoning in the README. README/AGENTS: fold the reach statement into the Security paragraph, and note that 1777 is not isolation between consumers — sticky stops them deleting each other's staged files, not reading them or adding their own; per-consumer subdirectories are the shape if that's ever needed. Say the mode is re-asserted on this package's start rather than continuously, since a consumer restart doesn't re-assert it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reviewed this — the diagnosis is right, and it holds up from the consumer side: One follow-up, plus a smoke test. I couldn't push to your branch (see the last section), so it's on a branch here instead: git remote add upstream https://github.com/Start9-Community/simplex-websocket-bridge-startos.git
git fetch upstream pr-11-review-fixups
git merge --ff-only upstream/pr-11-review-fixups
git push origin fix-ownerIt fast-forwards cleanly — it's your What's in it
This matters more than I first credited it: simplex-chat writes Rationale moved out of the source into the README. The comment in README / AGENTS. Folded the "only packages that declare this dependency" reach statement into the Security paragraph, where the rest of that argument already lives, and noted that Smoke testFresh install on an x86_64 dev box with no prior simplex or openclaw present: The service came up clean under the tightened parent — profile created, both DBs written, client settings synced over the WebSocket. I did not exercise the non-root send path, since openclaw isn't installed on that box — taking your hardware verification for that. Why I couldn't push to your branch — worth fixing for next time"Allow edits by maintainers" is enabled on this PR, but GitHub only provisions the actual push grant when the head repo is a true fork of the base repo. Your
After that, "Allow edits by maintainers" actually grants us push, and we can put review fixups straight onto your PRs instead of this branch-and-merge dance. Nothing to change on this PR for it — it only affects future ones. Thanks for the thorough writeup on the original commit, by the way — the reproduction and the "why mode, not ownership" reasoning made this quick to verify. |
|
Cool, thanks! After this PR closes, I will delete my original repo, which will make the Start9-Community repo the root and any future PRs from me will be editable. |
Problem
This container runs as root, so
.simplex/outboundwas created root-owned with0755. A consumer stages outgoing files as its own uid, so a non-root consumer can't write there and every send fails withEACCES.This became live rather than theoretical when
openclaw-startosswitched OpenClaw to run asnode(uid 1000) — necessary on their side, because OpenClaw's plugin loader only accepts plugin files owned by the gateway's own uid or root. On updating toopenclaw 2026.7.1:3the agent could still receive files but could no longer send them.Receiving was unaffected: inbound is mounted read-only and simplex-chat writes received files world-readable, so a non-root consumer reads them fine under the root-owned dir.
Fix
Create
.simplex/outboundwith mode1777— world-writable plus the sticky bit, as on/tmp— applied frommain.tson every start.Why mode and not ownership:
nodehappens to be OpenClaw's; chowning tonode:nodewould just relocate the breakage for the next consumer.Reach is unchanged: only packages that declare this dependency and mount the subpath can see the dir at all, and the contract already limits consumer write access to
outbound.Docs
The file-exchange contract specified paths and read/write access but was silent on uid and ownership — which is why this surfaced at runtime on a consumer rather than during review. Now documented in the README contract, the
mainMountsdoc comment, and the AI-consumer YAML block.Version
0.3.0:4→0.3.0:5(v0.3.0_4is tagged), with a release-note bullet in all five locales.Verification
outboundroot-owned, the agent could receive but not send. Hand-chowning it tonode:noderestored sending, confirming ownership/mode was the only blocker.outboundback toroot:root, started this build, and StartOS corrected the mode on its own. Sent a file end to end and confirmed the expected owner and mode on the staged file inoutbound.tsc --noEmitclean;ncc build startos/index.tssucceeds.