/** * Skill-scoped containment for the `/skills` file manager (design v3 §API). * * NOT `api/open-path.ts` `isInsideRoot`: that helper FOLLOWS symlinks (a `/link-to-outside` * resolves to its real target, which is the right question for "may I open this file the run * produced" and the wrong one for "may I write here") and accepts the root itself. Writes into the * effective plugin root need the stricter rule: * * 1. the URL wildcard arrives DECODED EXACTLY ONCE — by Fastify (find-my-way decodes every `%xx`, * `%2F` included, before routing; a malformed escape is Fastify's own 400) — and the store * never decodes again (codex round 5 on #480: a second decode turned `100%25.txt` into a * refused `100%.txt` and the literal filename `a%252Fb.txt` into the path `a/b.txt`); * 2. normalize into segments — refuse empty, `.`, `..`, backslashes, NUL, absolute / drive-prefixed; * 3. lstat-walk EVERY existing component and refuse a symlink anywhere on the way (the plugin * tree carries none; one appearing is either an attack or an accident, and either way the * write must not follow it); * 4. the root itself is not a file — an empty remainder is refused. * * Atomic tmp+rename writes are the store's job (`tree.ts` `writeFileAtomic`); this module only * decides WHERE. And the walk is necessary, not sufficient (design v3.5 §3; codex round 8): the * path it answers is then OPENED with `O_NOFOLLOW` where the platform has it, and the lstat-then-open * gap is closed everywhere with a post-open identity check (`fstat` dev/ino against the lstat result * — `tree.ts` `openRegularNoFollow`); copies read their source through such an open, never by * path; a staged tree is re-walked and re-hashed after the copy and before the rename into place. * Residual, documented: a directory component swapped between the walk and the rename by another * writer with access to the crew-owned state home — mitigated by the single-writer daemon and the * post-operation verification, not eliminated. */ /** * `reserved`: a name the store itself owns at that level (`snapshot.json`, `manifest.json`, `current`, * `views/`, `.venv`). `outside-closure`: a support path outside the bundle closure — the ONE allowlist * of what the seed copies and a snapshot may carry (bundle.ts `inBundleClosure`; codex round 6). */ export type SkillPathReason = 'invalid' | 'symlink' | 'nested-skill' | 'root' | 'reserved' | 'outside-closure'; /** A skill-relative path the store refuses. `reason` names why; the route answers 400 with `message`. */ export declare class SkillPathError extends Error { readonly reason: SkillPathReason; constructor(reason: SkillPathReason, message: string); } /** * Validate a POSIX-relative path into its segments. Rejects: empty, NUL, backslashes (paths are * POSIX; a backslash is never a separator here and never a filename), absolute / drive-prefixed, * and `.` / `..` / empty segments. */ export declare function validateRelSegments(rel: string): string[]; /** * `join(root, ...segments)` after an lstat walk refusing any symlink component — the walk itself * is `tree.ts` `assertNoSymlinkComponents` (the one no-follow rule every store path goes * through); this wrapper only speaks the file manager's error type. `root` is the SKILLS root: * the store passes `effective//` (or `baseline//…`) as segments, so the * skill directory itself is a walked component — replacing it with a link is refused, not * followed. A component that does not exist yet ends the walk (a not-yet-written leaf, or its new * parent dirs) — the write creates them. Any filesystem error other than ENOENT propagates: an * unreadable component is not judged lexically. */ export declare function containedPath(root: string, segments: ReadonlyArray): string; //# sourceMappingURL=contain.d.ts.map