@senior-skills/installer 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,71 @@
1
+ import { type AgentTarget, type Clock, type FileSystem, type Hasher, type InstallRequest, type InstallResult, type InstallationEngine, type InstalledSkill, type Logger, type UninstallRequest, type UninstallResult } from '@senior-skills/core';
2
+ export interface AtomicInstallerOptions {
3
+ readonly fs: FileSystem;
4
+ readonly hasher: Hasher;
5
+ readonly clock: Clock;
6
+ readonly logger: Logger;
7
+ /** Recorded in receipts, e.g. `@senior-skills/cli@1.0.0`. */
8
+ readonly toolVersion: string;
9
+ }
10
+ /**
11
+ * The only component in the system that writes into an agent's skill directory.
12
+ *
13
+ * Concentrating writes here is what makes the security and atomicity guarantees checkable:
14
+ * every adapter, present and future, gets the same hardened commit path for free, and the
15
+ * adversarial tests only have to cover one implementation.
16
+ *
17
+ * Commit sequence:
18
+ *
19
+ * 1. stage into `<root>/.senior-skills-staging-xxxx` — same filesystem as the target, so
20
+ * the final move is a rename and not a copy
21
+ * 2. move any existing install aside to `<root>/.senior-skills-retired-xxxx`
22
+ * 3. rename staging into place
23
+ * 4. delete the retired copy
24
+ *
25
+ * A crash at any step leaves either the old version or the new one intact. Step 2 is undone
26
+ * if step 3 fails, so a failed install never destroys a working skill.
27
+ */
28
+ export declare class AtomicInstaller implements InstallationEngine {
29
+ private readonly fs;
30
+ private readonly hasher;
31
+ private readonly clock;
32
+ private readonly logger;
33
+ private readonly toolVersion;
34
+ constructor(options: AtomicInstallerOptions);
35
+ install(request: InstallRequest): Promise<InstallResult>;
36
+ /**
37
+ * Swap the staged directory into place, keeping the previous install recoverable until the
38
+ * new one is committed.
39
+ */
40
+ private commit;
41
+ /**
42
+ * A `file`-shaped layout must project to exactly one file; the installer names it, so the
43
+ * entry's own path is not used.
44
+ */
45
+ private planSingleFile;
46
+ private planFiles;
47
+ private buildReceipt;
48
+ /**
49
+ * A second copy of the receipt lives in the skill root rather than only inside the skill
50
+ * directory, so `list` and `doctor` can enumerate installs with one directory read instead
51
+ * of descending into every skill.
52
+ */
53
+ private writeRootReceipt;
54
+ private result;
55
+ uninstall(request: UninstallRequest): Promise<UninstallResult>;
56
+ /**
57
+ * Depth-first removal of directories that became empty, stopping at the first that did not.
58
+ * Returns true when `directory` itself was removed.
59
+ */
60
+ private pruneEmptyDirectories;
61
+ list(target: AgentTarget): Promise<readonly InstalledSkill[]>;
62
+ /** The package name a root entry stands for, or undefined when it is not one. */
63
+ private nameOf;
64
+ /** Where the receipt's file paths are relative to: the package directory, or the root. */
65
+ private baseFor;
66
+ read(target: AgentTarget, name: string): Promise<InstalledSkill | undefined>;
67
+ private describe;
68
+ private hasDrifted;
69
+ private readReceipt;
70
+ }
71
+ //# sourceMappingURL=atomic-installer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-installer.d.ts","sourceRoot":"","sources":["../src/atomic-installer.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,KAAK,WAAW,EAChB,KAAK,KAAK,EACV,KAAK,UAAU,EACf,KAAK,MAAM,EAEX,KAAK,cAAc,EACnB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EAEnB,KAAK,MAAM,EAEX,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAM7B,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,eAAgB,YAAW,kBAAkB;IACxD,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAQ;IAC9B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,OAAO,EAAE,sBAAsB;IAQrC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC;IA0F9D;;;OAGG;YACW,MAAM;IA2BpB;;;OAGG;IACH,OAAO,CAAC,cAAc;IA+BtB,OAAO,CAAC,SAAS;IA0CjB,OAAO,CAAC,YAAY;IA0BpB;;;;OAIG;YACW,gBAAgB;IAM9B,OAAO,CAAC,MAAM;IAqBR,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC;IAqEpE;;;OAGG;YACW,qBAAqB;IAsB7B,IAAI,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,cAAc,EAAE,CAAC;IAiBnE,iFAAiF;IACjF,OAAO,CAAC,MAAM;IAMd,0FAA0F;IAC1F,OAAO,CAAC,OAAO;IAIT,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;YAMpE,QAAQ;YAkCR,UAAU;YAUV,WAAW;CAc1B"}
@@ -0,0 +1,392 @@
1
+ import { AgentSkillsError, ErrorCode, RECEIPT_DIRNAME, classifyChange, entryNameFor, parseReceipt, posix, receiptPath, stringifyReceipt, } from '@senior-skills/core';
2
+ import { assertContained, safeRelativePath } from "./safe-path.js";
3
+ const STAGING_PREFIX = '.senior-skills-staging-';
4
+ const RETIRED_PREFIX = '.senior-skills-retired-';
5
+ /**
6
+ * The only component in the system that writes into an agent's skill directory.
7
+ *
8
+ * Concentrating writes here is what makes the security and atomicity guarantees checkable:
9
+ * every adapter, present and future, gets the same hardened commit path for free, and the
10
+ * adversarial tests only have to cover one implementation.
11
+ *
12
+ * Commit sequence:
13
+ *
14
+ * 1. stage into `<root>/.senior-skills-staging-xxxx` — same filesystem as the target, so
15
+ * the final move is a rename and not a copy
16
+ * 2. move any existing install aside to `<root>/.senior-skills-retired-xxxx`
17
+ * 3. rename staging into place
18
+ * 4. delete the retired copy
19
+ *
20
+ * A crash at any step leaves either the old version or the new one intact. Step 2 is undone
21
+ * if step 3 fails, so a failed install never destroys a working skill.
22
+ */
23
+ export class AtomicInstaller {
24
+ fs;
25
+ hasher;
26
+ clock;
27
+ logger;
28
+ toolVersion;
29
+ constructor(options) {
30
+ this.fs = options.fs;
31
+ this.hasher = options.hasher;
32
+ this.clock = options.clock;
33
+ this.logger = options.logger;
34
+ this.toolVersion = options.toolVersion;
35
+ }
36
+ async install(request) {
37
+ const { adapter, target, pkg } = request;
38
+ const layout = adapter.layoutFor(pkg);
39
+ const entryName = safeRelativePath(entryNameFor(target, pkg.manifest.name), `${adapter.id} layout`);
40
+ const destination = this.fs.join(target.root, entryName);
41
+ const previous = await this.read(target, pkg.manifest.name);
42
+ if (previous !== undefined && previous.unmanaged && !request.force) {
43
+ throw new AgentSkillsError(ErrorCode.MODIFIED_INSTALL, `${destination} already exists and was not installed by senior-skills`, {
44
+ details: ['Overwriting it would destroy content this tool does not own.'],
45
+ hints: [`senior-skills install ${pkg.manifest.name} --force to replace it anyway`],
46
+ data: { name: pkg.manifest.name, directory: destination },
47
+ });
48
+ }
49
+ if (previous !== undefined && previous.modified && !request.force) {
50
+ throw new AgentSkillsError(ErrorCode.MODIFIED_INSTALL, `${pkg.manifest.name} has local modifications in ${destination}`, {
51
+ details: ['Files have changed since they were installed.'],
52
+ hints: [`senior-skills install ${pkg.manifest.name} --force to discard the changes`],
53
+ data: { name: pkg.manifest.name, directory: destination },
54
+ });
55
+ }
56
+ // File-shaped installs record their one file relative to the root, because there is no
57
+ // package directory for it to be relative to.
58
+ const files = target.shape === 'file'
59
+ ? this.planSingleFile(layout.entries, pkg, entryName, adapter.id)
60
+ : this.planFiles(layout.entries, pkg, destination);
61
+ const outcome = decideOutcome(previous, request);
62
+ if (request.dryRun) {
63
+ return this.result(request, destination, files, previous, outcome, this.buildReceipt(request, destination, files));
64
+ }
65
+ await this.fs.mkdirp(target.root);
66
+ const staging = await this.fs.makeTempDir(target.root, STAGING_PREFIX);
67
+ try {
68
+ for (const file of files) {
69
+ const absolute = this.fs.join(staging, ...file.path.split('/'));
70
+ assertContained(staging, this.fs.resolve(absolute), this.fs.separator);
71
+ await this.fs.mkdirp(this.fs.dirname(absolute));
72
+ // Written without an executable bit: `scripts/` ships as data, and this installer
73
+ // never runs anything a package contains.
74
+ await this.fs.writeFile(absolute, file.bytes);
75
+ }
76
+ const receipt = this.buildReceipt(request, destination, files);
77
+ if (target.shape === 'directory') {
78
+ await this.fs.writeFile(this.fs.join(staging, RECEIPT_DIRNAME, 'receipt.json'), stringifyReceipt(receipt));
79
+ }
80
+ // Both shapes commit by rename: a directory moves as a whole, a single file replaces
81
+ // the previous one, and the staging directory is left empty behind it.
82
+ await this.commit(target.shape === 'file' ? this.fs.join(staging, entryName) : staging, destination, target.root);
83
+ if (target.shape === 'file')
84
+ await this.fs.remove(staging).catch(() => undefined);
85
+ await this.writeRootReceipt(target, receipt);
86
+ return this.result(request, destination, files, previous, outcome, receipt);
87
+ }
88
+ catch (error) {
89
+ await this.fs.remove(staging).catch(() => undefined);
90
+ throw error;
91
+ }
92
+ }
93
+ /**
94
+ * Swap the staged directory into place, keeping the previous install recoverable until the
95
+ * new one is committed.
96
+ */
97
+ async commit(staging, destination, root) {
98
+ const exists = await this.fs.exists(destination);
99
+ let retired;
100
+ if (exists) {
101
+ retired = this.fs.join(root, `${RETIRED_PREFIX}${Date.now().toString(36)}`);
102
+ await this.fs.rename(destination, retired);
103
+ }
104
+ try {
105
+ await this.fs.rename(staging, destination);
106
+ }
107
+ catch (error) {
108
+ // Put the old version back before surfacing the failure: the invariant is that a failed
109
+ // install leaves the existing version untouched.
110
+ if (retired !== undefined) {
111
+ await this.fs.rename(retired, destination).catch(() => {
112
+ this.logger.error(`Install failed and the previous version could not be restored. It is preserved at ${retired}`);
113
+ });
114
+ }
115
+ throw error;
116
+ }
117
+ if (retired !== undefined)
118
+ await this.fs.remove(retired).catch(() => undefined);
119
+ }
120
+ /**
121
+ * A `file`-shaped layout must project to exactly one file; the installer names it, so the
122
+ * entry's own path is not used.
123
+ */
124
+ planSingleFile(entries, pkg, entryName, agentId) {
125
+ if (entries.length !== 1) {
126
+ throw new AgentSkillsError(ErrorCode.INTERNAL, `Adapter "${agentId}" projected ${entries.length} files onto a single-file target`, { data: { agentId, entries: entries.length } });
127
+ }
128
+ const entry = entries[0];
129
+ let bytes;
130
+ if (entry.content !== undefined) {
131
+ bytes = entry.content;
132
+ }
133
+ else {
134
+ const source = pkg.files.find((file) => file.path === posix.normalize(entry.copyFrom));
135
+ if (source === undefined) {
136
+ throw new AgentSkillsError(ErrorCode.INTERNAL, `Adapter asked to copy "${entry.copyFrom}", which is not in the package`, { data: { path: entry.copyFrom } });
137
+ }
138
+ bytes = source.bytes;
139
+ }
140
+ return [{ path: entryName, bytes }];
141
+ }
142
+ planFiles(entries, pkg, destination) {
143
+ const planned = new Map();
144
+ for (const entry of entries) {
145
+ const relative = safeRelativePath(entry.path, destination);
146
+ if (posix.segments(relative)[0] === RECEIPT_DIRNAME) {
147
+ throw new AgentSkillsError(ErrorCode.UNSAFE_PATH, `A package may not write into ${RECEIPT_DIRNAME}/`, {
148
+ details: [`Offending path: ${entry.path}`, 'That directory holds install bookkeeping.'],
149
+ data: { path: entry.path },
150
+ });
151
+ }
152
+ let bytes;
153
+ if (entry.content !== undefined) {
154
+ bytes = entry.content;
155
+ }
156
+ else {
157
+ const source = pkg.files.find((file) => file.path === posix.normalize(entry.copyFrom));
158
+ if (source === undefined) {
159
+ throw new AgentSkillsError(ErrorCode.INTERNAL, `Adapter asked to copy "${entry.copyFrom}", which is not in the package`, { data: { path: entry.copyFrom } });
160
+ }
161
+ bytes = source.bytes;
162
+ }
163
+ planned.set(relative, { path: relative, bytes });
164
+ }
165
+ return [...planned.values()].sort((a, b) => a.path.localeCompare(b.path));
166
+ }
167
+ buildReceipt(request, destination, files) {
168
+ return {
169
+ receiptVersion: 1,
170
+ name: request.pkg.manifest.name,
171
+ version: request.pkg.manifest.version,
172
+ agentId: request.target.agentId,
173
+ scope: request.target.scope,
174
+ registry: request.registry,
175
+ resolved: request.resolved,
176
+ integrity: request.integrity,
177
+ installedAt: this.clock.now().toISOString(),
178
+ installedWith: this.toolVersion,
179
+ directory: destination,
180
+ files: files.map((file) => ({
181
+ path: file.path,
182
+ integrity: this.hasher.hash(file.bytes),
183
+ size: file.bytes.byteLength,
184
+ })),
185
+ dependencyOf: request.dependencyOf,
186
+ };
187
+ }
188
+ /**
189
+ * A second copy of the receipt lives in the skill root rather than only inside the skill
190
+ * directory, so `list` and `doctor` can enumerate installs with one directory read instead
191
+ * of descending into every skill.
192
+ */
193
+ async writeRootReceipt(target, receipt) {
194
+ const path = this.fs.join(target.root, ...receiptPath(receipt.name).split('/'));
195
+ await this.fs.mkdirp(this.fs.dirname(path));
196
+ await this.fs.writeFile(path, stringifyReceipt(receipt));
197
+ }
198
+ result(request, destination, files, previous, outcome, receipt) {
199
+ return {
200
+ outcome,
201
+ name: request.pkg.manifest.name,
202
+ version: request.pkg.manifest.version,
203
+ ...(previous === undefined ? {} : { previousVersion: previous.version }),
204
+ agentId: request.target.agentId,
205
+ scope: request.target.scope,
206
+ directory: destination,
207
+ files: files.map((file) => file.path),
208
+ receipt,
209
+ };
210
+ }
211
+ async uninstall(request) {
212
+ const { target, name } = request;
213
+ const installed = await this.read(target, name);
214
+ if (installed === undefined) {
215
+ throw new AgentSkillsError(ErrorCode.NOT_INSTALLED, `"${name}" is not installed for ${target.agentId}`, {
216
+ data: { name, agentId: target.agentId, root: target.root },
217
+ });
218
+ }
219
+ if (installed.unmanaged) {
220
+ throw new AgentSkillsError(ErrorCode.MODIFIED_INSTALL, `${installed.directory} was not installed by senior-skills`, {
221
+ details: ['Refusing to delete files this tool does not own.'],
222
+ hints: ['Remove the directory manually if you are sure'],
223
+ data: { name, directory: installed.directory },
224
+ });
225
+ }
226
+ const receipt = await this.readReceipt(target, name);
227
+ const removed = [];
228
+ const preserved = [];
229
+ const base = this.baseFor(target, installed.directory);
230
+ for (const file of receipt?.files ?? []) {
231
+ const absolute = this.fs.join(base, ...file.path.split('/'));
232
+ if (!(await this.fs.exists(absolute)))
233
+ continue;
234
+ // Only delete what still matches what we wrote. Anything the user edited is theirs.
235
+ const current = await this.hasher.hashFile(absolute).catch(() => undefined);
236
+ if (current !== file.integrity && !request.force) {
237
+ preserved.push(file.path);
238
+ continue;
239
+ }
240
+ if (!request.dryRun)
241
+ await this.fs.remove(absolute);
242
+ removed.push(file.path);
243
+ }
244
+ if (!request.dryRun) {
245
+ await this.fs
246
+ .remove(this.fs.join(target.root, ...receiptPath(name).split('/')))
247
+ .catch(() => undefined);
248
+ if (target.shape === 'directory') {
249
+ await this.fs
250
+ .remove(this.fs.join(installed.directory, RECEIPT_DIRNAME))
251
+ .catch(() => undefined);
252
+ // Prune only what is now empty. A blanket delete of the skill directory would take
253
+ // files the user added after installation, which the receipt never claimed.
254
+ await this.pruneEmptyDirectories(installed.directory);
255
+ }
256
+ }
257
+ return {
258
+ name,
259
+ ...(receipt === undefined ? {} : { version: receipt.version }),
260
+ agentId: target.agentId,
261
+ scope: target.scope,
262
+ directory: installed.directory,
263
+ removed: removed.sort(),
264
+ preserved: preserved.sort(),
265
+ };
266
+ }
267
+ /**
268
+ * Depth-first removal of directories that became empty, stopping at the first that did not.
269
+ * Returns true when `directory` itself was removed.
270
+ */
271
+ async pruneEmptyDirectories(directory) {
272
+ if (!(await this.fs.exists(directory)))
273
+ return true;
274
+ let entries;
275
+ try {
276
+ entries = await this.fs.readDir(directory);
277
+ }
278
+ catch {
279
+ return false;
280
+ }
281
+ for (const entry of entries) {
282
+ if (!entry.isDirectory)
283
+ continue;
284
+ await this.pruneEmptyDirectories(this.fs.join(directory, entry.name));
285
+ }
286
+ const remaining = await this.fs.readDir(directory).catch(() => [{ name: 'unknown' }]);
287
+ if (remaining.length > 0)
288
+ return false;
289
+ await this.fs.remove(directory).catch(() => undefined);
290
+ return true;
291
+ }
292
+ async list(target) {
293
+ if (!(await this.fs.exists(target.root)))
294
+ return [];
295
+ const skills = [];
296
+ for (const entry of await this.fs.readDir(target.root)) {
297
+ if (entry.name.startsWith('.'))
298
+ continue; // .senior-skills, staging and retired leftovers
299
+ const name = this.nameOf(target, entry.name, entry.isDirectory);
300
+ if (name === undefined)
301
+ continue;
302
+ const skill = await this.describe(target, name);
303
+ if (skill !== undefined)
304
+ skills.push(skill);
305
+ }
306
+ return skills.sort((a, b) => a.name.localeCompare(b.name));
307
+ }
308
+ /** The package name a root entry stands for, or undefined when it is not one. */
309
+ nameOf(target, entryName, isDirectory) {
310
+ if (target.shape === 'directory')
311
+ return isDirectory ? entryName : undefined;
312
+ if (isDirectory || !entryName.endsWith(target.extension))
313
+ return undefined;
314
+ return entryName.slice(0, entryName.length - target.extension.length);
315
+ }
316
+ /** Where the receipt's file paths are relative to: the package directory, or the root. */
317
+ baseFor(target, destination) {
318
+ return target.shape === 'file' ? target.root : destination;
319
+ }
320
+ async read(target, name) {
321
+ const directory = this.fs.join(target.root, entryNameFor(target, name));
322
+ if (!(await this.fs.exists(directory)))
323
+ return undefined;
324
+ return this.describe(target, name);
325
+ }
326
+ async describe(target, name) {
327
+ const directory = this.fs.join(target.root, entryNameFor(target, name));
328
+ const receipt = await this.readReceipt(target, name);
329
+ if (receipt === undefined) {
330
+ // A skill directory the tool has no receipt for. Reported, never touched.
331
+ return {
332
+ name,
333
+ version: '0.0.0',
334
+ agentId: target.agentId,
335
+ scope: target.scope,
336
+ directory,
337
+ registry: 'unknown',
338
+ installedAt: '',
339
+ unmanaged: true,
340
+ modified: false,
341
+ dependencyOf: [],
342
+ };
343
+ }
344
+ return {
345
+ name: receipt.name,
346
+ version: receipt.version,
347
+ agentId: target.agentId,
348
+ scope: target.scope,
349
+ directory,
350
+ registry: receipt.registry,
351
+ installedAt: receipt.installedAt,
352
+ unmanaged: false,
353
+ modified: await this.hasDrifted(this.baseFor(target, directory), receipt),
354
+ dependencyOf: receipt.dependencyOf,
355
+ };
356
+ }
357
+ async hasDrifted(base, receipt) {
358
+ for (const file of receipt.files) {
359
+ const absolute = this.fs.join(base, ...file.path.split('/'));
360
+ if (!(await this.fs.exists(absolute)))
361
+ return true;
362
+ const current = await this.hasher.hashFile(absolute).catch(() => undefined);
363
+ if (current !== file.integrity)
364
+ return true;
365
+ }
366
+ return false;
367
+ }
368
+ async readReceipt(target, name) {
369
+ const path = this.fs.join(target.root, ...receiptPath(name).split('/'));
370
+ if (!(await this.fs.exists(path)))
371
+ return undefined;
372
+ try {
373
+ return parseReceipt(await this.fs.readTextFile(path), path);
374
+ }
375
+ catch (error) {
376
+ this.logger.warn(`Ignoring unreadable install receipt ${path}`);
377
+ this.logger.debug(String(error));
378
+ return undefined;
379
+ }
380
+ }
381
+ }
382
+ function decideOutcome(previous, request) {
383
+ if (previous === undefined || previous.unmanaged)
384
+ return 'installed';
385
+ const change = classifyChange(previous.version, request.pkg.manifest.version);
386
+ if (change === 'downgrade')
387
+ return 'downgraded';
388
+ if (change === 'same')
389
+ return previous.modified ? 'reinstalled' : 'unchanged';
390
+ return 'upgraded';
391
+ }
392
+ //# sourceMappingURL=atomic-installer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-installer.js","sourceRoot":"","sources":["../src/atomic-installer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@senior-skills/installer` — the single write path into an agent's skill directory.
3
+ *
4
+ * Everything security-critical about turning an untrusted package into files on disk lives
5
+ * in this package: path safety, archive extraction limits, and the atomic commit.
6
+ */
7
+ export { AtomicInstaller, type AtomicInstallerOptions } from './atomic-installer.ts';
8
+ export { DEFAULT_EXTRACTION_LIMITS, SafeExtractor, type ExtractionLimits, } from './safe-extractor.ts';
9
+ export { MAX_PATH_SEGMENTS, MAX_SEGMENT_LENGTH, assertContained, inspectPath, isSafePath, safeRelativePath, type UnsafePathReason, } from './safe-path.ts';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAErF,OAAO,EACL,yBAAyB,EACzB,aAAa,EACb,KAAK,gBAAgB,GACtB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,UAAU,EACV,gBAAgB,EAChB,KAAK,gBAAgB,GACtB,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `@senior-skills/installer` — the single write path into an agent's skill directory.
3
+ *
4
+ * Everything security-critical about turning an untrusted package into files on disk lives
5
+ * in this package: path safety, archive extraction limits, and the atomic commit.
6
+ */
7
+ export { AtomicInstaller } from "./atomic-installer.js";
8
+ export { DEFAULT_EXTRACTION_LIMITS, SafeExtractor, } from "./safe-extractor.js";
9
+ export { MAX_PATH_SEGMENTS, MAX_SEGMENT_LENGTH, assertContained, inspectPath, isSafePath, safeRelativePath, } from "./safe-path.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,eAAe,EAA+B,MAAM,uBAAuB,CAAC;AAErF,OAAO,EACL,yBAAyB,EACzB,aAAa,GAEd,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,UAAU,EACV,gBAAgB,GAEjB,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { type ArchiveEntry, type ArchiveReader, type SkillPackage } from '@senior-skills/core';
2
+ export interface ExtractionLimits {
3
+ readonly maxFiles: number;
4
+ readonly maxTotalBytes: number;
5
+ /** Reject archives whose uncompressed size exceeds this multiple of the compressed size. */
6
+ readonly maxCompressionRatio: number;
7
+ }
8
+ export declare const DEFAULT_EXTRACTION_LIMITS: ExtractionLimits;
9
+ /**
10
+ * Turns an untrusted `.tar.gz` into a validated {@link SkillPackage} in memory.
11
+ *
12
+ * Nothing here writes to disk. Extraction happens entirely in memory, and only a package that
13
+ * survives every check is handed to the installer for staging — so a hostile archive never
14
+ * gets the chance to create a file at all, rather than being cleaned up after the fact.
15
+ *
16
+ * Rejected outright:
17
+ * - symlinks and hardlinks (any entry type other than file or directory)
18
+ * - paths that are absolute, traversing, UNC, drive-lettered, or Windows-hostile
19
+ * - duplicate entries for the same path (last-write-wins is how a check gets bypassed)
20
+ * - archives over the size, count or compression-ratio limits
21
+ */
22
+ export declare class SafeExtractor {
23
+ private readonly archives;
24
+ private readonly limits;
25
+ constructor(archives: ArchiveReader, limits?: ExtractionLimits);
26
+ extract(data: Uint8Array, source: string): Promise<SkillPackage>;
27
+ fromEntries(entries: readonly ArchiveEntry[], source: string, compressedBytes: number): SkillPackage;
28
+ }
29
+ //# sourceMappingURL=safe-extractor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-extractor.d.ts","sourceRoot":"","sources":["../src/safe-extractor.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,YAAY,EACjB,KAAK,aAAa,EAElB,KAAK,YAAY,EAClB,MAAM,qBAAqB,CAAC;AAG7B,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,4FAA4F;IAC5F,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;CACtC;AAED,eAAO,MAAM,yBAAyB,EAAE,gBAIvC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgB;IACzC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;gBAE9B,QAAQ,EAAE,aAAa,EAAE,MAAM,GAAE,gBAA4C;IAKnF,OAAO,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAKtE,WAAW,CACT,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,MAAM,EAAE,MAAM,EACd,eAAe,EAAE,MAAM,GACtB,YAAY;CAuFhB"}
@@ -0,0 +1,120 @@
1
+ import { AgentSkillsError, ErrorCode, MANIFEST_FILENAME, buildSkillPackage, decodeText, parseManifest, } from '@senior-skills/core';
2
+ import { safeRelativePath } from "./safe-path.js";
3
+ export const DEFAULT_EXTRACTION_LIMITS = {
4
+ maxFiles: 2_000,
5
+ maxTotalBytes: 32 * 1024 * 1024,
6
+ maxCompressionRatio: 200,
7
+ };
8
+ /**
9
+ * Turns an untrusted `.tar.gz` into a validated {@link SkillPackage} in memory.
10
+ *
11
+ * Nothing here writes to disk. Extraction happens entirely in memory, and only a package that
12
+ * survives every check is handed to the installer for staging — so a hostile archive never
13
+ * gets the chance to create a file at all, rather than being cleaned up after the fact.
14
+ *
15
+ * Rejected outright:
16
+ * - symlinks and hardlinks (any entry type other than file or directory)
17
+ * - paths that are absolute, traversing, UNC, drive-lettered, or Windows-hostile
18
+ * - duplicate entries for the same path (last-write-wins is how a check gets bypassed)
19
+ * - archives over the size, count or compression-ratio limits
20
+ */
21
+ export class SafeExtractor {
22
+ archives;
23
+ limits;
24
+ constructor(archives, limits = DEFAULT_EXTRACTION_LIMITS) {
25
+ this.archives = archives;
26
+ this.limits = limits;
27
+ }
28
+ async extract(data, source) {
29
+ const entries = await this.archives.read(data);
30
+ return this.fromEntries(entries, source, data.byteLength);
31
+ }
32
+ fromEntries(entries, source, compressedBytes) {
33
+ const files = new Map();
34
+ let totalBytes = 0;
35
+ const prefix = commonLeadingComponent(entries);
36
+ for (const entry of entries) {
37
+ if (entry.type === 'directory')
38
+ continue;
39
+ if (entry.type !== 'file') {
40
+ throw new AgentSkillsError(ErrorCode.UNSAFE_ARCHIVE, `Refusing archive: "${entry.path}" is a ${entry.type}`, {
41
+ details: [
42
+ entry.linkTarget === undefined ? '' : `-> ${entry.linkTarget}`,
43
+ 'Skill packages may contain regular files only.',
44
+ ].filter((line) => line !== ''),
45
+ hints: ['Links can redirect writes outside the install directory'],
46
+ data: { source, path: entry.path, type: entry.type },
47
+ });
48
+ }
49
+ // Archives produced by GitHub and by `npm pack` wrap the package in one directory.
50
+ // Strip it only when *every* entry shares it, so a flat archive is left alone.
51
+ const relative = safeRelativePath(stripPrefix(entry.path, prefix), source);
52
+ if (files.has(relative)) {
53
+ throw new AgentSkillsError(ErrorCode.UNSAFE_ARCHIVE, `Refusing archive: duplicate entry "${relative}"`, {
54
+ details: ['A duplicated path lets a later entry overwrite an already-checked file.'],
55
+ data: { source, path: relative },
56
+ });
57
+ }
58
+ totalBytes += entry.bytes.byteLength;
59
+ if (files.size >= this.limits.maxFiles) {
60
+ throw new AgentSkillsError(ErrorCode.UNSAFE_ARCHIVE, `Refusing archive: more than ${this.limits.maxFiles} files`, { data: { source, maxFiles: this.limits.maxFiles } });
61
+ }
62
+ if (totalBytes > this.limits.maxTotalBytes) {
63
+ throw new AgentSkillsError(ErrorCode.UNSAFE_ARCHIVE, `Refusing archive: expands to more than ${this.limits.maxTotalBytes} bytes`, { data: { source, maxTotalBytes: this.limits.maxTotalBytes } });
64
+ }
65
+ files.set(relative, { path: relative, bytes: entry.bytes });
66
+ }
67
+ if (compressedBytes > 0 && totalBytes / compressedBytes > this.limits.maxCompressionRatio) {
68
+ throw new AgentSkillsError(ErrorCode.UNSAFE_ARCHIVE, 'Refusing archive: compression ratio looks like a decompression bomb', {
69
+ details: [
70
+ `compressed: ${compressedBytes} bytes`,
71
+ `uncompressed: ${totalBytes} bytes`,
72
+ `ratio: ${Math.round(totalBytes / compressedBytes)}:1 (limit ${this.limits.maxCompressionRatio}:1)`,
73
+ ],
74
+ data: { source, compressedBytes, totalBytes },
75
+ });
76
+ }
77
+ const manifestFile = files.get(MANIFEST_FILENAME);
78
+ if (manifestFile === undefined) {
79
+ throw new AgentSkillsError(ErrorCode.INVALID_PACKAGE, `Archive from ${source} contains no ${MANIFEST_FILENAME}`, { data: { source, entries: [...files.keys()] } });
80
+ }
81
+ const { manifest } = parseManifest(decodeText(manifestFile.bytes), {
82
+ source: `${source}/${MANIFEST_FILENAME}`,
83
+ });
84
+ return buildSkillPackage(manifest, [...files.values()]);
85
+ }
86
+ }
87
+ function unify(path) {
88
+ return path.replace(/\\/g, '/').replace(/^\.\//, '');
89
+ }
90
+ /**
91
+ * The single directory every entry lives under, or undefined when there is not exactly one.
92
+ *
93
+ * A traversal component is never treated as a strippable prefix: `../evil` must reach the
94
+ * safety check intact rather than being quietly turned into `evil`.
95
+ */
96
+ function commonLeadingComponent(entries) {
97
+ const heads = new Set();
98
+ for (const entry of entries) {
99
+ if (entry.type !== 'file')
100
+ continue;
101
+ const unified = unify(entry.path);
102
+ const slash = unified.indexOf('/');
103
+ if (slash <= 0)
104
+ return undefined; // a root-level file means there is nothing to strip
105
+ heads.add(unified.slice(0, slash));
106
+ if (heads.size > 1)
107
+ return undefined;
108
+ }
109
+ const [only] = [...heads];
110
+ if (only === undefined || only === '.' || only === '..')
111
+ return undefined;
112
+ return only;
113
+ }
114
+ function stripPrefix(path, prefix) {
115
+ const unified = unify(path);
116
+ if (prefix === undefined)
117
+ return unified;
118
+ return unified.startsWith(`${prefix}/`) ? unified.slice(prefix.length + 1) : unified;
119
+ }
120
+ //# sourceMappingURL=safe-extractor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-extractor.js","sourceRoot":"","sources":["../src/safe-extractor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import { inspectPath, isSafePath, MAX_PATH_SEGMENTS, MAX_SEGMENT_LENGTH, type PathSafetyViolation } from '@senior-skills/core';
2
+ /**
3
+ * Path safety at the extraction boundary.
4
+ *
5
+ * The rules themselves live in `@senior-skills/core` so that `validate` and the extractor
6
+ * cannot disagree about what is safe. This module adds what only the installer needs: turning
7
+ * a violation into a thrown security error, and proving containment against a real directory.
8
+ */
9
+ export { MAX_PATH_SEGMENTS, MAX_SEGMENT_LENGTH, inspectPath, isSafePath };
10
+ export type UnsafePathReason = PathSafetyViolation;
11
+ /**
12
+ * Validates and normalises an untrusted path to a POSIX relative path.
13
+ * Throws rather than sanitising: silently rewriting a hostile path hides the attack.
14
+ */
15
+ export declare function safeRelativePath(raw: string, context?: string): string;
16
+ /**
17
+ * Second line of defence: proves the *resolved* destination is still inside the root.
18
+ *
19
+ * The lexical rules cannot see through a symlinked parent directory, so the installer also
20
+ * compares resolved paths before writing. Belt and braces, because the cost of being wrong
21
+ * here is arbitrary file overwrite.
22
+ */
23
+ export declare function assertContained(root: string, candidate: string, separator: string): void;
24
+ //# sourceMappingURL=safe-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-path.d.ts","sourceRoot":"","sources":["../src/safe-path.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,WAAW,EACX,UAAU,EAEV,iBAAiB,EACjB,kBAAkB,EAClB,KAAK,mBAAmB,EACzB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;GAMG;AACH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;AAC1E,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAEnD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAUtE;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAgBxF"}
@@ -0,0 +1,51 @@
1
+ import { AgentSkillsError, ErrorCode, inspectPath, isSafePath, normalizeSafePath, MAX_PATH_SEGMENTS, MAX_SEGMENT_LENGTH, } from '@senior-skills/core';
2
+ /**
3
+ * Path safety at the extraction boundary.
4
+ *
5
+ * The rules themselves live in `@senior-skills/core` so that `validate` and the extractor
6
+ * cannot disagree about what is safe. This module adds what only the installer needs: turning
7
+ * a violation into a thrown security error, and proving containment against a real directory.
8
+ */
9
+ export { MAX_PATH_SEGMENTS, MAX_SEGMENT_LENGTH, inspectPath, isSafePath };
10
+ /**
11
+ * Validates and normalises an untrusted path to a POSIX relative path.
12
+ * Throws rather than sanitising: silently rewriting a hostile path hides the attack.
13
+ */
14
+ export function safeRelativePath(raw, context) {
15
+ const reason = inspectPath(raw);
16
+ if (reason !== undefined) {
17
+ throw new AgentSkillsError(ErrorCode.UNSAFE_PATH, `Refusing unsafe path: ${truncate(raw)}`, {
18
+ details: [reason.message, ...(context === undefined ? [] : [`In: ${context}`])],
19
+ hints: ['This package is malformed or hostile; do not install it'],
20
+ data: { path: raw, rule: reason.rule, ...(context === undefined ? {} : { context }) },
21
+ });
22
+ }
23
+ return normalizeSafePath(raw);
24
+ }
25
+ /**
26
+ * Second line of defence: proves the *resolved* destination is still inside the root.
27
+ *
28
+ * The lexical rules cannot see through a symlinked parent directory, so the installer also
29
+ * compares resolved paths before writing. Belt and braces, because the cost of being wrong
30
+ * here is arbitrary file overwrite.
31
+ */
32
+ export function assertContained(root, candidate, separator) {
33
+ const normalizedRoot = stripTrailing(root, separator);
34
+ const normalizedCandidate = stripTrailing(candidate, separator);
35
+ if (normalizedCandidate === normalizedRoot)
36
+ return;
37
+ if (normalizedCandidate.startsWith(normalizedRoot + separator))
38
+ return;
39
+ throw new AgentSkillsError(ErrorCode.UNSAFE_PATH, 'Refusing to write outside the destination directory', {
40
+ details: [`destination: ${normalizedRoot}`, `resolved to: ${normalizedCandidate}`],
41
+ hints: ['A symlink in the target path may be redirecting the install'],
42
+ data: { root: normalizedRoot, candidate: normalizedCandidate },
43
+ });
44
+ }
45
+ function stripTrailing(value, separator) {
46
+ return value.endsWith(separator) ? value.slice(0, -separator.length) : value;
47
+ }
48
+ function truncate(value) {
49
+ return value.length > 120 ? `${value.slice(0, 117)}...` : value;
50
+ }
51
+ //# sourceMappingURL=safe-path.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"safe-path.js","sourceRoot":"","sources":["../src/safe-path.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,WAAW,EACX,UAAU,EACV,iBAAiB,EACjB,iBAAiB,EACjB,kBAAkB,GAEnB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;GAMG;AACH,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;AAG1E;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW,EAAE,OAAgB;IAC5D,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAChC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,gBAAgB,CAAC,SAAS,CAAC,WAAW,EAAE,yBAAyB,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE;YAC1F,OAAO,EAAE,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC,CAAC;YAC/E,KAAK,EAAE,CAAC,yDAAyD,CAAC;YAClE,IAAI,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE;SACtF,CAAC,CAAC;IACL,CAAC;IACD,OAAO,iBAAiB,CAAC,GAAG,CAAC,CAAC;AAChC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,SAAiB,EAAE,SAAiB;IAChF,MAAM,cAAc,GAAG,aAAa,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IACtD,MAAM,mBAAmB,GAAG,aAAa,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAEhE,IAAI,mBAAmB,KAAK,cAAc;QAAE,OAAO;IACnD,IAAI,mBAAmB,CAAC,UAAU,CAAC,cAAc,GAAG,SAAS,CAAC;QAAE,OAAO;IAEvE,MAAM,IAAI,gBAAgB,CACxB,SAAS,CAAC,WAAW,EACrB,qDAAqD,EACrD;QACE,OAAO,EAAE,CAAC,gBAAgB,cAAc,EAAE,EAAE,iBAAiB,mBAAmB,EAAE,CAAC;QACnF,KAAK,EAAE,CAAC,6DAA6D,CAAC;QACtE,IAAI,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,mBAAmB,EAAE;KAC/D,CACF,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CAAC,KAAa,EAAE,SAAiB;IACrD,OAAO,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;AAC/E,CAAC;AAED,SAAS,QAAQ,CAAC,KAAa;IAC7B,OAAO,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC;AAClE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@senior-skills/installer",
3
+ "version": "1.0.0",
4
+ "description": "Atomic, rollback-safe skill installation engine",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/robsonkades/senior-skills.git",
9
+ "directory": "packages/installer"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.js",
13
+ "types": "./dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist"
22
+ ],
23
+ "engines": {
24
+ "node": ">=22.18.0"
25
+ },
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "dependencies": {
30
+ "@senior-skills/core": "1.0.0"
31
+ }
32
+ }