--- title: Manifest reference description: What every manifest field means, the values it accepts, and the validation rules. section: dev order: 20 updated: 2026-11-09 translation_of: zh source_sha: 95a3be7439db --- # Manifest reference ## Descriptions The gem itself, each of its tools, each SOP, and each agent must have a description (`description`). Descriptions are written for the AI and for users, and they have length limits: | Where | Limit | What to write | | --- | --- | --- | | Top-level `description` in the manifest | 200 characters | One or two sentences: what the gem can do and what its main tools are called (for a gem with only a skill, what the skill covers). | | `tools[].description` | 512 characters | What the tool does and when to use it. | | `workflows[].description` (SOP) | 200 characters | What the SOP does and when it runs. | | `description` in an agent file's header | No limit | One or two sentences: what this agent is for; shown on the gem's page. See [agents](#agents). | | `description` of an exposed tool, SOP, or agent | 200 characters | Items marked `expose: true` go into the main chat's system prompt, so they share the 200-character limit of the gem description. See [Exposing items](#exposing-items-expose). | Limits are counted in characters; one Chinese character counts as one. A missing description, an empty one, or one over the limit fails manifest validation and the package can't be built (`E_PERM_INVALID`). An agent's description has no length limit; Gem Studio refuses to install an agent that has none. Keep the gem's description short, because it is the only thing the AI has to go on to get to know this gem: every gem the user has installed is listed in front of the AI as one line (name, id, description), even when there are hundreds. The AI decides from that one line alone whether to open the gem, and only after opening it can it see the tools, SOPs, and skill (see [What a gem is](/en/dev/gem-overview.md#how-the-ai-uses-your-gem)). So the gem's description covers only what it does and its main tools; usage and details go in the descriptions of the tools and SOPs themselves. The gem store's listing and search will use this same text later. ```json "description": "Summarizes and fully translates every new email and keeps both in an encrypted store. Main tools: summarize_email, translate_email, save_record, list_records." ``` In a Gem Studio draft, the gem's description is the top-level `description` in `manifest.json`, and each tool's or SOP's description is written in the `description` of the corresponding `exports[]` entry. When there is only one tool and the top-level shorthand is used, the top-level `description` is both the gem's and the tool's description, so it also can't exceed 200 characters. An agent's description goes in the header of its own file. ## tools The tools the gem provides for the AI and for SOPs to call. ```json "tools": [ { "name": "summarize_email", "entry": "tools/summarize_email.js", "description": "Distill one email into its key points and any action items. Returns { summary }.", "inputSchema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] }, "outputSchema": { "type": "object", "properties": { "summary": { "type": "string" } } } } ] ``` | Field | Required | Description | | --- | --- | --- | | `name` | Yes | Starts with a lowercase letter and contains only lowercase letters, digits, and underscores, up to 64 characters. SOPs call it with `ctx.call('tool.<name>', …)`. | | `entry` | Yes | The file that holds the tool's code. For how to write it, see the [Tool API](/en/dev/tools.md). | | `description` | Yes | Up to 512 characters; see "Descriptions" above. | | `inputSchema` | Yes | JSON Schema for the input parameters. File and folder paths are ordinary strings; a tool reads and writes in the conversation folder with the `workspace:` prefix, see [Permission model](/en/dev/permissions.md#conversation-folder). | | `outputSchema` | No | JSON Schema for the return value, for gems and SOPs that call this tool later. It is not checked at present. | | `permissions` | No | The permissions this tool itself uses, see [Permission model](/en/dev/permissions.md). The gem's own storage doesn't need to be listed (see [Storage is not a permission](/en/dev/permissions.md#storage-is-not-a-permission)). | | `sideEffect` | No | `read` (changes nothing; the only kind of tool that can run in plan mode), `write` (treated as this when omitted), or `destructive` (hard to undo: deleting, sending, paying). This is your declaration and is not checked at run time: write `read` only when the tool truly changes nothing. | | `expose` | No | `true`: exposed, so the main chat's AI can see and call it. When omitted the tool is for the gem's own use: only this gem's SOPs, agents, and pages can call it, and calls from any other session are refused. See [Exposing items](#exposing-items-expose). | Tools have no time limit of their own: the user can stop one at any time, and calls inside a SOP are bound by the SOP's `limits.maxDurationMs`. ## Exposing items (expose) An installed gem works in two directions: it gets tasks done through its own pages, SOPs, and agents, and it acts as a teammate with a specialty, letting the main chat's AI (and any agent outside the gem) put its skills to use. The latter works by the gem **exposing** a small, explicit set of interfaces: the tools, SOPs, and agents marked `expose: true`. Everything else is the gem's internal implementation; agents outside the gem can't see it or call it (Notochord refuses the call whatever permissions the caller has). What the main chat's AI sees is: one line per installed gem (its `description`), and under it one line per exposed item (its `description`). After the AI opens the gem with `read_skill`, these are exactly the items that show up in its tool list; on the first call the user is asked once, and the approval covers the whole gem. So the exposed items and their descriptions are everything this gem is in the team's eyes. Write them in words a teammate would understand (what it does for someone, when to use it), not implementation steps. What to expose: | Form | When | How to write it | | --- | --- | --- | | Tool | One call does the whole job and the result is a single value: look something up, convert, summarize one email, record an entry. It costs no extra model call, and the input format keeps calls accurate. | Add `"expose": true` to that entry in `tools[]` | | agent | Several of the gem's own tools must be chained and the gem's judgment is needed (filing taxes: collect, classify, calculate, fill in forms). The main chat hands it the task in a paragraph (`gem.<id>.<name>({ task })`), and it completes it in its own session, with its own `tools` and `platform_tools`, using the model the user chose for it; its reply is the result. It sees only this task, not the rest of the conversation, so the task has to carry everything it needs. The gem's many internal tools stay inside. | Add `expose: true` to the agent file's header | | SOP | A fixed procedure a teammate can start (it may wait for approval or for a schedule, in which case the caller first gets a run number). | Add `"expose": true` to that entry in `workflows[]` | | Nothing | A gem that only gives the team knowledge to read (it has a skill: the main chat can read `skill/SKILL.md` anyway), or one that works only through its own pages and scheduled SOPs. | Write none of them | Both at install time and in Gem Studio these are checked: each gem can expose at most 5 items; an exposed item's description is at most 200 characters (it goes into the system prompt). A gem that has generated many tools for its own pages and SOPs should expose one or two of them, or one agent, rather than the whole list. Exposed names can't repeat across the gem's tools, SOPs, and agents (they share the single `gem.<id>.<name>` namespace, so a tool, SOP, and agent can never share a name anyway). ## skill A skill is instructions written for the AI: how to get things done with this gem, meaning which tool to use in which situation, in what order, and what to watch out for. It is only text; it is not executed and needs no permissions. **A gem has at most one skill**, in the same format as [Agent Skills](https://agentskills.io/specification): a `SKILL.md`, plus the reference material it mentions. ``` skill/ SKILL.md references/ regions.md rules/ 2024.csv ``` - **`skill/SKILL.md`**: the skill itself. The AI reads it in full as soon as it opens this gem, so keep it short (at most 64 KB, the shorter the better): what the gem can do, the main approach, and **when to read which reference**, written as the full path from the gem root, for example `skill/references/regions.md`. No header (front matter) is needed; the AI picks gems by the top-level `description` in the manifest. - **`skill/references/`**: the details that `SKILL.md` mentions, with any subfolders you like: steps, examples, tables, data. **Text files only**: `.md`, `.txt`, `.json`, `.jsonl`, `.csv`, `.tsv`, `.yaml`, `.yml`, `.xml`, each at most 64 KB. No runnable files (code, scripts); if you need a code example, put it in a Markdown file. - `skill/` can contain nothing else, and `references/` can't exist without a `SKILL.md`. A package that doesn't comply can't be built (`E_PERM_INVALID`). **Nothing to declare in the manifest.** If the package has `skill/SKILL.md`, the gem has a skill. **Notochord doesn't check whether `SKILL.md` has the reference paths right.** A reference that isn't mentioned won't be read by the AI; a wrong path can't be read. Check it yourself after writing: every path mentioned in `SKILL.md` is under `skill/references/`, and every reference says in `SKILL.md` when to read it. In Gem Studio you can ask the assistant to help check. **A gem can have only a skill**: no tools, SOPs, or agents, just `skill/`, for example a company's writing guidelines or a description of a business process. It still appears in the AI's gem list, and the AI can read these files with `read_skill`. **How the AI reads it**: the AI reads your gem id with `read_skill` and gets all of these at once: - The **full text** of `SKILL.md`; - The **name of every file** under `skill/references/` (`<gem id>/skill/references/…`); - Your gem's tools (including SOPs), which appear in the AI's tool list from the next step on. After that, whenever the AI needs a reference, it reads the full text with `read_skill` and that name. Reading a skill doesn't ask the user. **The gem's own agents read it the same way**: as long as the gem has a skill, each of its agents gets `read_skill`, and the names of all the skill's files are appended to the end of the system prompt. What an agent reads with `read_skill` is only text; the tools it can use are still only those listed in its own file header. ### How to write a skill - **Put instructions and an index in `SKILL.md`** and the details in `references/`: `SKILL.md` is read into the conversation in full every time the gem is opened. - **Rules to follow every time** go in the agent's own prompt (the body of `agents/<name>.md`), which the gem's agent sees on every turn. The Chat agent reads the skill only when it opens your gem. - **Don't have too many or too fragmented references**: each file appears as one line in the list, so hundreds of small files make the list itself eat a lot of context, and small models find it harder to pick correctly. ## permissions.knowledge Request access to the user's knowledge base. For the full description see the [Knowledge API](/en/dev/knowledge.md). ```json "permissions": { "knowledge": { "permissions": ["facts.read:self:location.home.*", "collections.write:diary"], "predicates": [{ "predicate": "pref.food.cuisine", "cardinality": "many", "description": "Cuisines the user likes", "aliases": ["cuisine"] }], "reason": "A one-sentence reason shown to the user (optional)" } } ``` | Field | Required | Description | | --- | --- | --- | | `permissions` | Yes | A list of permission strings; for the format and accepted values see [Permission model](/en/dev/permissions.md). Each one is validated at install time, and a manifest with a malformed entry can't be installed. | | `predicates` | No | The predicates this gem will write. `predicate` must be "a fixed first level plus at least one more level", for example `pref.food.cuisine`; `cardinality` is `one` or `many`. They are written into the global vocabulary at install time, and ones that already exist are not overwritten. | | `reason` | No | Shown on the install screen. | A tool that needs `ctx.knowledge` writes `"knowledge": true` in its own `permissions`. What it can access is decided by the gem-level declaration above and the user's approval; the tool level is only a switch. In a Gem Studio draft, write `"knowledge": { ... }` at the top level of `manifest.json`, and every tool can use it by default. When a tool needs only some of the items, write `"permissions": ["network", "knowledge"]` in its own entry (or check them on the tool's page in Gem Studio); `side_effect` is also written there. ## data What shape the gem's data should be stored in: which collections there are, and each collection's fields, indexes, and version. The platform stores data according to this declaration and validates every write; tools read and write with `ctx.data`, see [Data and storage](/en/dev/storage.md). ```json "data": { "collections": { "notes": { "kind": "table", "description": "To-dos the user has noted down", "fields": { "title": { "type": "string", "required": true }, "status": { "type": "string", "enum": ["open", "done"] }, "due": { "type": "timestamp" }, "project": { "type": "ref", "to": "projects" }, "tags": { "type": "array", "items": "string" } }, "indexes": [["status", "due"], ["project"]] }, "projects": { "kind": "table", "fields": { "name": { "type": "string", "required": true, "unique": true } } }, "readings": { "kind": "log", "fields": { "value": { "type": "number" } }, "retention": { "days": 365 } } } } ``` | Field | Required | Description | | --- | --- | --- | | `collections` | No | The collections; the key is the collection name, at most 64. | | `eventRetentionDays` | No | How many days to keep the change record (the record each write leaves behind: who changed it and what changed), 1–3650, default 180. Log entries aren't cleaned up by this; they follow their own `retention`. | Collection and field names: start with a lowercase letter, contain only lowercase letters, digits, and underscores, up to 64 characters. ### Collections Each collection is either a table (`table`: records that can be modified and queried) or a Log (`log`: entries that are append-only and never modified). | Field | Table | Log | Description | | --- | --- | --- | --- | | `kind` | Required | Required | `table` or `log`. | | `fields` | Required | Required | 1–128 fields, see "Fields" below. | | `description` | Optional | Optional | Up to 500 characters. | | `version` | Optional | Not allowed | An integer, default 1, which can only go up, never down; see "Rules for changing fields" below. | | `indexes` | Optional | Not allowed | Up to 16, each an array of 1–8 field names with no repeats; only scalar fields can be used (see the table below). For what indexes a query needs, see [Index rules](/en/dev/storage.md#index-rules). | | `migrations` | Required when `version` is greater than 1 | Not allowed | The path of the migration module in the package: `.js`, `.mjs`, or `.ts`, with no `..`, and the file must be in the package. It exports one function per step (`v1_to_v2(data, { id })`, `v2_to_v3`, …); see [Versions and migrations](/en/dev/storage.md#versions-and-migrations). | | `retention` | Not allowed | Optional | How much of the old entries to keep: `{ "days": 1–36500, "maxEntries": 1–10000000 }`, with at least one of them, cleaned up by the platform. If omitted, entries are kept forever. | ### Fields | Field | Description | | --- | --- | | `type` | Required. Scalars: `string`, `number`, `integer`, `boolean`, `timestamp` (integer milliseconds), `ref` (the id of another record), `blob` (a Blob's hash); these can be queried, sorted, and indexed. Three others: `array` (can only be queried with `array-contains`), `object`, and `any` (these two can't be queried). | | `required` | When `true`, every record must have this field. | | `unique` | When `true`, this field's value can't repeat within the collection. Only allowed on a table's scalar fields. | | `enum` | The allowed values, 1–256, with no repeats; only allowed on `string`, `number`, and `integer`, and the value type must match the field. | | `to` | Required for a `ref` field (or an array whose `items` is `ref`): which collection it points to, which must be a table in the same manifest. | | `items` | Only allowed on `array`: the element type, one of the seven scalar types. If omitted, elements aren't checked, and `array-contains` can't be used. | | `description` | Up to 500 characters. | ### Rules for changing fields Each time a gem is activated (install, upgrade, Gem Studio test), Notochord compares the declaration in the manifest with the data already stored: - **Within the same `version` you can only loosen**: add an optional field, remove `required` or `unique`, widen or remove an `enum`, add or drop indexes, change descriptions. Adding `unique` is also allowed, but activation fails if existing values contain duplicates. - **These require raising `version` and writing a migration**: removing a field; changing `type`, `to`, or `items`; adding `required` to an existing field; narrowing an `enum`, or adding one to a field that had none; adding a new required field. - `version` can't be lower than the data already stored. - A collection's `kind` can't be changed. - A Log has no version; its fields can be changed at any time, and old entries stay as they were when written. - A collection removed from the manifest keeps its data (the interface can't reach it), and the data is deleted on uninstall. Breaking these rules makes activation fail with `E_DATA_SCHEMA: …` (a newly added `unique` that meets duplicate values gives `E_DATA_UNIQUE: …`); on an upgrade or a reinstall of the same version it is wrapped in `E_ACTIVATION_FAILED`, and the upgrade rolls back to the old version (see [Packaging and publishing](/en/dev/publishing.md#what-is-checked-at-install)). If the `data` section itself is written wrongly, you get `E_PERM_INVALID` in Gem Studio tests, packaging, and installation. When packaging and installing, errors of shape (unknown keys, a wrong `type`, an overlong description) are caught first by the manifest's JSON Schema, with the message `notochord.json failed schema validation: …`; the other rules (names, count limits, `to`, `version` and `migrations`, a migration file missing from the package) have messages that begin with `data:`. In Gem Studio tests they all begin with `data:`. In a Gem Studio draft, `data` is likewise written at the top level of `manifest.json` with the same fields; edit `manifest.json` directly. For how test data is kept and how to test migrations, see [Testing in Gem Studio](/en/dev/storage.md#testing-in-gem-studio). ## agents The agents a gem brings along. **Each agent is one file under `agents/`**, `agents/<name>.md`: the file name is the agent's name, the YAML header (front matter) at the start says what it is for and which tools it can use, and the body after it is its system prompt. ```markdown --- description: Summarize and translate new emails, then file them. tools: [summarize_email, translate_email, save_record] platform_tools: [http.request] expose: true --- You are a calm, meticulous email assistant. … ``` **Agents aren't written in the manifest.** When packaging, Notochord reads every file under `agents/` and generates the `agents[]` in the package's manifest; if the `notochord.json` you wrote has `agents`, packaging reports an error. The same holds for Gem Studio drafts: there are no agents in `manifest.json`, and creating, renaming, or deleting an agent is creating, renaming, or deleting its file. **Name**: the file name without `.md`. Starts with a lowercase letter, contains only lowercase letters, digits, and underscores, up to 64 characters (`agents/mail_helper.md` is `mail_helper`). The file must be directly in `agents/`, not in a subfolder. SOPs call it with `ctx.call('agent.<name>', …)`, and a page's assistant bubble names it with `data-agent="<name>"`. The header can have only the following five keys; writing any other makes packaging report an error: | Key | Required | Description | | --- | --- | --- | | `description` | Yes | What this agent is for, in one or two sentences; shown on the gem's page. If it contains `: ` or `#`, put the whole sentence in quotes. | | `tools` | No | The gem's own tools it can call, written as tool names. | | `platform_tools` | No | The platform tools it can call, written with full names (`fs.cat`, `http.request`, `browser.navigate`, and so on). This is a permission request and is listed under "What it can do" on the gem's page. It can't include `tool_search`, `read_skill`, `agent.spawn_child`, or `search_gem_catalog`, nor another gem's tools (`gem.*`). | | `models` | No | The models its code uses; the key is a name, and `main` is the model it uses when conversing (present even if not written). Each entry has a single optional field, `requires`: what the model must be able to do besides text, see [Model capabilities](#model-capabilities) below. **The user chooses which model is used**, on the gem's page or in "Settings › AI Settings › Agents": a specific model, or one of the user's roles (the built-in ones are `chat`, `vision`, `deep`, and `local`; `local` is for models running on the user's own computer and is called "Local" in the interface). When nothing is chosen, the user's `chat` role is used; it can't be specified here. If you want to suggest what kind of model the user should use (for example, an agent that handles email or personal data should suggest "Local"), write it in the gem's introduction document (see [docs](#docs)). | | `expose` | No | `true`: exposed, so the main chat's AI hands tasks to it (see [Exposing items](#exposing-items-expose)). Then `description` is limited to 200 characters and counts toward the gem's limit of 5 items. If omitted, only this gem's SOPs and pages can use it. | Each item in the generated `agents[]` is `name`, `entry` (`agents/<name>.md`), `description`, `tools`, `platformTools`, `models`, `expose`. After installation, Notochord decides what an agent can use from `agents[]` alone; the file's header is no longer read, and the body is the system prompt. An agent can only call the tools listed in `tools` and `platform_tools`; anything else is unavailable. The one exception is `read_skill`: when the gem has a skill, Notochord automatically adds it to every agent, only for reading the skill (see [skill](#skill)), and it neither needs to nor may be written in `platform_tools`. ### Model capabilities When an agent needs a model to do something beyond text, such as looking at images or listening to audio, say so in that `models` entry with `requires`: ```yaml models: { main: { requires: [image_input] } } ``` Only the six values below are allowed; writing anything else means the package **can't be built** (`E_PERM_INVALID`): | Value | The model must be able to | | --- | --- | | `image_input` | Understand images | | `audio_input` | Understand audio, including speech-to-text | | `video_input` | Understand video | | `image_output` | Generate images | | `audio_output` | Generate audio, including speech synthesis | | `video_output` | Generate video | The user chooses a model for each agent on the gem's page (when nothing is chosen, the user's `chat` role is used) and can also change it in "Settings › AI Settings › Agents". Notochord checks the chosen model against a list of the capabilities of common models: - The list says the model has all the capabilities you wrote: nothing is shown. - The list says it lacks one of them, or the model isn't in the list (for example, a model name the user made up), or the role has no model configured yet: a line of notice is shown for the user to confirm. - The notice doesn't block installation, and nothing is checked at run time. When the model really doesn't support it, what fails is your agent's call to the model, exactly as if you hadn't written `requires`. So write only the capabilities the agent can't do its job without in `requires`. Don't write ones that are only occasionally needed, so users don't see the notice every time. **The manifest has no per-agent run limits.** How many steps an agent may take in one run and how many tokens it may use are decided by the user in "Settings › System" under "Maximum loop steps" and "Token limit per run", and are the same for all agents. The `maxTurns` and `maxCostUsd` fields have been removed: a packaged manifest that contains them is rejected at install time for failing manifest validation. ## docs `docs/` is written for people: what this gem can do and how to use it. **It becomes the gem's introduction page in the store.** Nothing in this folder is used while the gem is in use: the gem's code, its agents, and the AI don't read it, so don't put instructions for the AI here (that is the [skill](#skill)). ``` docs/ README.en.md README.zh-CN.md prompts/ first-steps.en.md first-steps.zh-CN.md examples/ weekly-report.en.md ``` - **Markdown only** (`.md`), with any subfolders. Gem Studio drops other files when packaging; a package you built yourself that contains other files fails both packaging and installation (`E_PERM_INVALID`). Images can't be included. - **Each file is named by language**, using standard language codes (BCP 47: `en`, `zh-CN`, `ja`, `de`, …): - `README.<language>.md`: the body of the introduction page: what this gem does, who it is for, how to set it up, how to use it. **The languages you write are exactly the languages in the language menu of the store's introduction page.** Write at least one (`README.en.md`). - `prompts/<name>.<language>.md`: optional, prompts that can be tried directly, each with a note on what it does. They get a separate "Prompts" tab on the store's introduction page. - `examples/<name>.<language>.md`: optional, complete examples of using this gem. They get a separate "Examples" tab on the store's introduction page. - The store renders these files as ordinary Markdown, and each tab shows the language the user chose. The language versions of the same prompt or example share one `<name>` (`prompts/first-steps.en.md`, `prompts/first-steps.zh-CN.md`). `docs/` doesn't need to be declared in the manifest. In Gem Studio, "Introduction docs" in the structure bar lists the files in this folder, and you can create and edit them there. ## resources Files that are too large to put in the install package (models, dictionaries, datasets). The manifest states their address, size, and checksum, and Notochord downloads them; a tool gets the local path with [`ctx.resources`](/en/dev/tools.md#ctxresources). ```json "resources": [ { "id": "ocr-model", "url": "https://cdn.example.com/models/ocr-v3.onnx", "mirrors": ["https://mirror.example.org/models/ocr-v3.onnx"], "sha256": "9f2c…", "size": 412000000, "required": true, "description": "OCR model" } ] ``` | Field | Required | Description | | --- | --- | --- | | `id` | Yes | The name used to get this file in a tool. It can't repeat within one manifest; a manifest with a repeat can't be installed. | | `url` | Yes | The download address, which must start with `https://`. | | `mirrors` | No | Fallback addresses, which also must be `https://`. They are tried one by one in order after `url` fails. | | `sha256` | Yes | The SHA-256 of the file's contents, 64 lowercase hex digits. | | `size` | Yes | The file size in bytes. | | `required` | No | `true`: the download starts in the background as soon as installation completes, and on every later app start it is tried again if the file isn't there yet. Default `false`: downloaded only when a tool first uses it. | | `description` | No | Shown on the gem's page. If omitted, the `id` is shown. | Downloads work like this: - **Notochord downloads, not the gem's code.** The download address doesn't need to be written into `network`, and no permission needs to be requested. The gem's page lists each file's description, size, which host it comes from, and whether it downloads right after install or only when used. - **Only `https` is accepted, and it can't point to a local or internal-network address.** Redirects are followed at most 5 times, and each hop is checked by the same rules. - **Subject to the user's network policy.** A host the user hasn't allowed in the network policy under "Settings › System" (allowlist / blocklist mode) can't be downloaded from; mirrors and hosts passed through by redirects count too. - **Both the size and the SHA-256 must match the manifest.** A file the server sends too much of, too little of, or with contents that don't match is discarded, and the next address is tried. If the remaining disk space isn't enough, the download doesn't start. - **A failed download doesn't affect installation.** The same goes for `required` files: installation doesn't wait for them and doesn't fail because of them. The gem works as usual, and only the call that uses this file reports an error; the download is retried automatically on the next call or the next app start. - **No repeated download on upgrade.** Files whose `sha256` is unchanged in the new version are reused; files the new version no longer declares are deleted after a successful upgrade. All are deleted when the gem is uninstalled. - Resuming an interrupted download is not supported; after a failure it starts over from the beginning. There is currently no download progress interface. In a Gem Studio draft, `resources` is likewise written at the top level of `manifest.json` with the same fields. You can also fill it in under "Resources" on the Gem Studio overview: after entering the address, click "Auto-fill size and SHA-256", and Notochord downloads the file by the same rules as downloading resources and computes those two values (the file isn't kept). During draft testing, `ctx.resources` works exactly as it does after installation.
Notochord
简体中文English