Commit34e0d78aRecorded17 Jul 2026Repositorysigil-system

Add fs-home: the machine's home directory, grant-checked

Message

A client that wants to expand "~" must not hardcode a host convention. The home that matters is the BROWSED machine's, which for a remote node (Familiar) is not the machine doing the typing — so it has to come from the capability surface like every other fact about that filesystem.

Grant-checked as a READ OF the home directory, deliberately: a principal scoped to /srv has no business learning where the owner's home is, and anything it would do with the answer (list it, read from it) is denied anyway. Under the in-process desktop posture (grant-allow-all) it is simply always allowed.

Raises when HOME is unset (a bare service environment); callers should read that as "no home to expand", not as fatal.

Ships in the same 0.1.1 as the canonical read-dir order.

Changed
 src/sigil/system/fs.sgl | 23 +++++++++++++++++++++++
 test/test-system.sgl    | 18 ++++++++++++++++++
 2 files changed, 41 insertions(+)
Diff
src/sigil/system/fs.sglmodified
@@ -28,9 +28,11 @@
28
(sigil path)
29
(sigil io)
30
(sigil string) ; string<? / string-downcase (listing order)
+31
(sigil process) ; getenv (fs-home)
32
(sigil system grant))
33
34
(export
+35
fs-home
36
fs-read-dir
37
fs-stat
38
fs-read-file
@@ -51,6 +53,27 @@
53
mtime: (stat-mtime st)
54
mode: (stat-mode st)))
55
+56
;;; The user's home directory, canonicalized. Raises when HOME is
+57
;;; unset (a bare service environment) — callers should treat that as
+58
;;; "no home to expand", not as fatal.
+59
;;;
+60
;;; Grant-checked as a READ OF that directory, deliberately: a
+61
;;; principal scoped to /srv has no business learning where the
+62
;;; owner's home is, and anything it would do with the answer (list
+63
;;; it, read from it) would be denied anyway. Under the in-process
+64
;;; desktop posture (grant-allow-all) this is simply always allowed.
+65
;;;
+66
;;; Exists so a client can expand "~" WITHOUT hardcoding a host
+67
;;; convention: the path belongs to the machine being browsed, which
+68
;;; for a remote node (Familiar) is not the machine typing.
+69
(define (fs-home g)
+70
(: any? -> string?)
+71
(let ((home (getenv "HOME")))
+72
(unless home
+73
(error "fs-home: HOME is not set"))
+74
(grant-assert! g 'fs-read home)
+75
(or (realpath home) home)))
+76
77
;;; Stat a file or directory. Returns a dict with `name:` `path:`
78
;;; `type:` `size:` `mtime:` `mode:`, or raises if the path does
79
;;; not exist.
test/test-system.sglmodified
@@ -8,6 +8,7 @@
8
(sigil io)
9
(sigil fs)
10
(sigil path)
+11
(sigil process) ; getenv (fs-home tests)
12
(sigil string)
13
(sigil async)
14
(sigil channels)
@@ -106,6 +107,23 @@
107
(fs-write-file g path "hi there\n")
108
(assert-equal (fs-read-file g path) "hi there\n"))))
109
+110
(test "fs-home returns the home directory when granted"
+111
;; Exists so a client can expand "~" without hardcoding a host convention —
+112
;; on a remote node the home that matters is the NODE's, not the one typing.
+113
(let ((g (make-grants))
+114
(home (getenv "HOME")))
+115
(grant-add! g (string-append "fs:ro:" home))
+116
(assert-equal (fs-home g) (or (realpath home) home))))
+117
+118
(test "fs-home is DENIED to a principal scoped elsewhere"
+119
;; Grant-checked as a read OF home on purpose: a principal scoped to a project
+120
;; directory has no business learning where the owner's home is, and everything
+121
;; it could do with the answer would be denied anyway.
+122
(let ((g (make-grants))
+123
(dir (make-temp-directory)))
+124
(grant-add! g (string-append "fs:rw:" dir))
+125
(assert-true (guard (e (#t #t)) (fs-home g) #f))))
+126
127
(test "read-dir returns stat dicts"
128
(let ((g (make-grants))
129
(dir (make-temp-directory)))