# `AgentDb.Skills.Source`
[🔗](https://github.com/manhvu/agent_db/blob/v0.1.0/lib/agent_db/skills/source.ex#L1)

Reading an Agent Skills bundle, and refusing one that cannot be stored as it is.

Every entry point -- the console's upload, a folder on disk, a tar archive --
arrives here, so what counts as a skill is decided in exactly one place. What
comes out is a list of skills, each a name and its files at the paths they had
inside the bundle, and nothing has been written anywhere.

## What a source may look like

  * one skill directory, with a `SKILL.md` at its root, and
  * a collection of immediate skill directories, each with a `SKILL.md`.

A folder that holds a `SKILL.md` at its own root is that one skill, and takes
its name from the folder. An archive may add one common wrapper directory
above the skills, which is how a repository is usually archived; it is
recognised only when every member sits beneath it and the skills are inside
it. The directory name of a skill is its name; nothing inside `SKILL.md` is
parsed, so a skill is preserved exactly as its author wrote it.

## What is refused

An unsafe path (absolute, traversing, backslashed, carrying control
characters, or holding an empty segment), a non-regular file, a symbolic or
hard link, a duplicate path, a path stored both as a file and a directory, a
skill with no `SKILL.md`, and a file that is not valid UTF-8. Files are read
as text because that is what the store holds; a binary asset is reported
rather than mangled.

## What is set aside

macOS metadata travels inside user folders -- AppleDouble `._` sidecars
(materialized beside every file on external volumes) and `.DS_Store` files.
It is never skill content, so entries whose base name is one are set aside
before they can cost budget, trip validation, or be stored. A source holding
only metadata is still refused, as a source with no skills.

Archive members are read in memory and never extracted to disk, and the two
limits in `limits/0` bound the entries and the expanded bytes of one source,
so a small archive cannot cost an unbounded amount of work.

## Why the limits are here and not in configuration

The console and the Mix task have to accept the same bundles, and a limit
that could be set differently on each would make that untrue. They are
therefore one set of numbers, published by `limits/0` so that a caller
configures its own caps to match rather than restating them.

# `file`

```elixir
@type file() :: %{path: [String.t()], content: String.t()}
```

One file of a skill, at `path` below the skill's root.

# `skill`

```elixir
@type skill() :: %{name: String.t(), files: [file()]}
```

A skill ready to be stored: its name, and every file it holds.

# `source`

```elixir
@type source() :: {:path, Path.t()} | {:archive, binary()} | {:uploads, [upload()]}
```

Where a bundle comes from.

`:path` is a directory or an archive on disk, `:archive` the bytes of one, and
`:uploads` the files of a browser directory selection.

# `upload`

```elixir
@type upload() :: %{path: String.t(), content: binary()}
```

One file as a browser sent it: a client-relative path and its bytes.

# `limits`

```elixir
@spec limits() :: %{max_entries: pos_integer(), max_bytes: pos_integer()}
```

The bounds one import accepts: entries, and expanded bytes.

# `load`

```elixir
@spec load(source()) :: {:ok, [skill()]} | {:error, term()}
```

Reads a source into the skills it holds.

A refusal is about the whole source, never about one file of it. Nothing is
written while a source is being read, so a rejected bundle cannot leave half
of itself stored, and `message/1` turns whatever comes back into a sentence
for the operator.

# `message`

```elixir
@spec message(term()) :: String.t()
```

Why an import was refused, as a sentence an operator can act on.

Every refusal an import can report is answered here, the destination's as well
as the source's, so a caller has one way to turn a reason into something worth
showing rather than a format of its own per surface.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
