1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
;;; Copyright (c) 2026 Nicholas Vermeulen
;;; SPDX-License-Identifier: AGPL-3.0-or-later
;; pkg.lisp — a registry-less package manager (Phase 5.2, pure Lisp).
;;
;; A PACKAGE is any git repository with a `package.lisp` manifest at its
;; root — one alist:
;; ((name "mylib") (version "0.1.0") (main "mylib.lisp")
;; (deps ("https://github.com/user/otherlib" ...))) ; deps optional
;;
;; Install = `git clone` into ~/.rusty/packages/<name>. Any URL git
;; understands works — https, ssh, file:// — so there is no central
;; registry to stand up or trust (the Go-modules lesson); a hosted index
;; can layer on later without changing this format.
;;
;; (pkg-install url [tag]) clone (optionally at a tag) + install deps
;; (pkg-load name) load an installed package's main file
;; (require-package url [tag]) install-if-missing, then load
;; (pkg-list) / (pkg-remove name)
;; The file `pkg-load` runs: the manifest's `main`, defaulting to <name>.lisp.
;; pkg-load and pkg-effects both go through this, so "the file that runs" and
;; "the file we inspect" can never drift apart.
;; ── Effect disclosure (what a package ADMITS to doing) ─────────────────────
;; Integrity (below) proves SAMENESS. This asks an orthogonal question: what
;; does this package's code admit it does? check-effects walks a body WITHOUT
;; running it; here we point it at the very file pkg-load would execute. The
;; file's top-level forms are wrapped in one (lambda () ...) — building the
;; closure runs nothing — and the real Rust effect walker reports every
;; effectful operation it finds (shell, file I/O, load, ...).
;;
;; HONEST SCOPE — this is DISCLOSURE, never a sandbox:
;; 1. It inspects the MAIN file only. A (load "other.lisp") inside it is
;; itself surfaced as an effect ("there is more code here I did not read"),
;; but that other file's contents are not walked.
;; 2. It surfaces every effect NAMED anywhere in the main file — including
;; inside a helper this file defines (that body is walked too). The blind
;; spot is an effect reachable only through code NOT in this file: a
;; DEPENDENCY's function, or a file this one `load`s. check-effects is
;; conservative — an unrecognized/user-defined call is never flagged — so
;; it reports what the file ADMITS, and can never prove what it CANNOT do.
;; 3. A pass from pkg-load-checked means "admits nothing beyond what you
;; allowed", never "safe to run".
;; The deduped effect OPERATION symbols (the same shape undeclared-effects /
;; safe-call use): #f if not installed, '() if pure, else e.g. (shell load).
;; Effect-gated load: load only if every operation the package admits is in
;; `allowed` (a list of op symbols). Otherwise refuse WITHOUT loading and name
;; the operations you did not allow. Refuse-by-disclosure, not a sandbox.
;; ── Integrity ────────────────────────────────────────────────────────────
;; What you install from a git URL is whatever that URL served you. A
;; fingerprint gives an installed package a comparable identity: every file
;; under it with its SHA-256, sorted by path (dir-list sorts, so two machines
;; that installed the same bytes agree exactly).
;;
;; HONEST SCOPE, twice over:
;; 1. A fingerprint proves SAMENESS, not safety. Identical bytes to a
;; malicious package are still malicious. It answers "is this what I
;; installed / what the publisher meant?" — never "is this good?".
;; 2. A fingerprint is only worth WHERE IT CAME FROM. One the package hands
;; you itself — in its own manifest, in its own repo — proves nothing:
;; whoever changes the files changes the manifest in the same commit.
;; That is why there is no (files ...) hash key in the manifest format;
;; it would look like a guarantee and be none. An expected fingerprint has
;; to reach you another way: the publisher's site, a release note, a
;; counterparty, or your own record from install day (that last one is
;; what pkg-lock! keeps, and it detects local drift only — an attacker on
;; your machine can rewrite the lock as easily as the package).
;;
;; .git is skipped: two clones of the same commit are not byte-identical there,
;; so including it would make every fingerprint disagree with every other.
;; Symlinks are followed (file-hash follows them), so a package that symlinks
;; outside its own tree fingerprints its target's bytes, not the link.
;; No is-a-directory? builtin exists; dir-list raises on a regular file, and
;; that raise is the test.
;; -> ((relpath sha256) ...) sorted, or #f if not installed
;; Name what moved, per file: changed / missing / added. A verdict of "this
;; package differs" without saying WHERE is not worth printing.
;; 'verified | (changed ((path what) ...)) | (not-installed name)
;; Locks live OUTSIDE the package directory on purpose: a lock inside the tree
;; it vouches for would be rewritten by the same `git pull` (or hand-edit) it
;; is supposed to notice, and would change its own fingerprint besides.
;; Has anything under an installed package changed since it was installed?
;; 'verified | (changed ...) | (no-lock name) | (not-installed name)
;; Give the package manager its own help category (embedded + auto-loaded into
;; every env, so these appear in (help)/(apropos)/(command-registry) unprefixed).
;; Without this the pkg-* names all fall into "other".