# IDOP Format Specification 1.0

| | |
| --- | --- |
| **Status** | Candidate Recommendation — container and manifest frozen; editorial changes only |
| **Date** | 2026-10-01 |
| **This version** | `https://idoplabs.com/spec/idop/1.0/` |
| **Media types** | `application/vnd.idop+zip` (package), `application/vnd.idop.bundle+zip` (bundle) |
| **File extensions** | `.idop`, `.idopbundle` |
| **Editor** | IDOP LABS |
| **Feedback** | `spec@idoplabs.com` |
| **License** | Text: CC BY 4.0. Schemas and conformance files: MIT OR Apache-2.0 |

## Abstract

IDOP is a file format for **interactive documents**. One `.idop` file carries a
document's user interface (HTML, CSS, JavaScript), its passive resources, and
its portable state. A conforming Reader validates the whole file before any of
its code runs, executes that code in an isolated sandbox with no network access
unless the user grants it, and writes changed state back into the file as a new
revision. Secrets never enter the file.

This specification defines the package container, the manifest, the executable
boundary, the storage and revision model, the Runtime API available to document
code, the permission model for network access, the `.idopbundle` transport, and
the requirements a Reader must meet.

## Table of contents

1. Conformance
2. Terminology
3. Overview
4. Package container
5. Paths
6. Limits
7. Manifest
8. Executable boundary
9. Reader processing model
10. Storage, Save and revisions
11. Runtime API
12. Capabilities, credentials and permissions
13. Portable Items (extension)
14. Bundles and containers
15. Versioning and compatibility
16. Error codes
17. Security considerations
18. Privacy considerations
19. Accessibility considerations
20. IANA considerations
21. References
Annex A — JSON Schemas · Annex B — Conformance suite · Annex C — Example · Annex D — Changes from the drafts

---

## 1. Conformance

The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHALL**, **SHALL NOT**,
**SHOULD**, **SHOULD NOT**, **RECOMMENDED**, **MAY** and **OPTIONAL** are to be
interpreted as described in BCP 14 [RFC 2119] [RFC 8174] when, and only when,
they appear in all capitals.

This specification defines three conformance classes:

- **Package** — a file that satisfies sections 4–8 and, where it uses them,
  sections 10–14.
- **Producer** — software that writes packages (an editor, a generator, an AI
  tool, the `idop pack` command). A Producer MUST write only conforming
  packages, SHOULD write them deterministically (§4.4), and MUST NOT write a
  secret into a package.
- **Reader** — software that opens packages and runs them. A Reader MUST
  implement section 9 in full. A Reader that runs document code MUST implement
  sections 10–12 and 17.1. A Reader MAY refuse any package it cannot process
  fully; it MUST NOT process a package partially.

Sections marked *(informative)*, examples and notes are not normative.

## 2. Terminology

- **Package** — a ZIP archive conforming to this specification, normally with
  the extension `.idop`.
- **Document** — what a person sees when a package is opened: the running
  application together with its state.
- **Manifest** — the JSON file `idop.json` at the package root.
- **Entry point** — the HTML file under `code/` that the Reader loads first.
- **Document code** — every entry under `code/`.
- **State** — the entries under `storage/`, the last saved portable state.
- **Overlay** — the Reader's in-memory copy of state during a session (§10).
- **Revision** — one saved version of a document, identified by a UUID.
- **Capability** — a permission the document declares and the user may grant,
  in 1.0 always network access to exact origins (§12).
- **Credential binding** — a declared need for a secret the Reader holds and the
  document never sees.
- **Origin** — scheme, host and port, as defined by [RFC 6454]; in this
  specification always `https`.
- **Session** — one opening of one document by a Reader.

## 3. Overview *(informative)*

```text
my-board.idop                     (ZIP)
├── mimetype                      application/vnd.idop+zip   first entry, Stored
├── idop.json                     manifest
├── code/                         document code: HTML, JS, CSS, JSON
│   ├── index.html                entry point
│   └── app.js
├── resources/                    passive files: images, fonts, data
├── storage/                      saved state, written by the Reader on Save
└── _idop/                        reserved for this specification
```

The design follows two established formats: the container identification of
OpenDocument and EPUB (a Stored `mimetype` first entry), and the web platform's
origin model for isolation. What IDOP adds is the contract between the two: the
file says what it wants, the Reader decides what it gets, and the user is asked
before anything leaves the device.

## 4. Package container

### 4.1 ZIP profile

A package MUST be a ZIP archive [APPNOTE] with these restrictions:

1. No bytes MAY precede the first local file header or follow the end of
   central directory record. (Self-extracting and polyglot files are refused.)
2. ZIP64, multi-volume and spanned archives MUST NOT be used.
3. Encryption MUST NOT be used.
4. Every entry MUST use compression method 0 (Store) or 8 (Deflate).
5. Entry names that contain non-ASCII characters MUST set the UTF-8 flag
   (general purpose bit 11), and every name MUST be valid UTF-8.
6. The local header and central directory record of each entry MUST agree on
   name, flags, method, sizes and CRC-32.
7. No two central directory records MAY reference the same local header.
8. Directory entries, symbolic links, devices and other special entries MUST NOT
   appear. Directories exist only as prefixes of file names.
9. The central directory MUST be contiguous and immediately precede the end of
   central directory record.

### 4.2 The `mimetype` entry

The first local file header MUST start at offset 0 and MUST describe an entry
named exactly `mimetype`, stored with method 0, without an extra field, whose
content is exactly the ASCII string:

```text
application/vnd.idop+zip
```

with no byte order mark, no trailing whitespace and no line terminator
(`IDOP-PROFILE-002`). Because the entry has no extra field (`IDOP-ZIP-019`) and
its name is eight bytes long, the media type sits at byte offset 38 of every
IDOP 1.0 package — which is what lets software identify the file without
decompressing it (§20).

### 4.3 Roots

Every entry name MUST begin with one of these roots:

| Root | Role | Executable |
| --- | --- | --- |
| `mimetype` | container identification | no |
| `idop.json` | manifest | no |
| `code/**` | document code: `.html`, `.js`, `.mjs`, `.css`, `.json` | **yes** |
| `resources/**` | passive resources | no |
| `storage/**` | last saved portable state | no |
| `_idop/**` | reserved for this specification | no |
| `extensions/<namespace>/**` | data of extensions | no |

Any other root MUST cause the package to be refused (`IDOP-PATH-010`).
`<namespace>` MUST be a lowercase reverse-domain identifier
(`com.example.feature`) followed by at least one child path segment
(`IDOP-PATH-012`).

The following names under `_idop/` are reserved for future versions of this
specification. A 1.0 Reader MUST ignore them, and a 1.0 Producer MUST NOT write
them except as defined by a later version:

| Reserved | Intended use |
| --- | --- |
| `_idop/signatures/` | publisher signatures over the package (planned for 1.1) |
| `_idop/thumbnail.png` | a preview image for file managers and galleries |
| `_idop/encryption/` | encryption metadata |

### 4.4 Deterministic writing

A Producer SHOULD write packages deterministically: `mimetype` first, `idop.json`
second, remaining entries in byte-wise lexicographic order of their names, a
fixed modification time, and fixed permissions. Packing the same inputs twice
SHOULD produce byte-identical files. *(The reference `idop pack` does.)*

## 5. Paths

Every entry name, and every path used by the Runtime API, MUST satisfy:

1. relative, `/`-separated, encoded as UTF-8 and normalised to Unicode NFC
   (`IDOP-PATH-004`);
2. at most 240 bytes and at most 16 segments (`IDOP-PATH-001`, `IDOP-PATH-005`);
3. no leading `/`, no `\`, no UNC or drive prefix such as `C:`
   (`IDOP-PATH-002`, `IDOP-PATH-009`);
4. no empty, `.` or `..` segment (`IDOP-PATH-006`);
5. no NUL or control character, no `:` and no segment ending in `.` or a space
   (`IDOP-PATH-003`, `IDOP-PATH-007`);
6. no segment that is a Windows device name (`CON`, `PRN`, `AUX`, `NUL`,
   `COM1`–`COM9`, `LPT1`–`LPT9`, with or without an extension) (`IDOP-PATH-008`);
7. no two entries whose names are equal after NFC normalisation and Unicode case
   folding (`IDOP-PATH-030`).

These rules make every package extractable on every common file system without
any entry escaping, colliding with or replacing another.

## 6. Limits

A Reader MUST accept any package within these limits and MAY refuse any package
that exceeds them:

| Limit | Value | Error |
| --- | ---: | --- |
| compressed package size | 64 MiB | `IDOP-LIMIT-001` |
| number of entries | 4 096 | `IDOP-LIMIT-002` |
| uncompressed size of one entry | 16 MiB | `IDOP-LIMIT-003` |
| compression ratio of one entry | 100 : 1 | `IDOP-LIMIT-004` |
| total uncompressed size | 128 MiB | `IDOP-LIMIT-005` |
| `idop.json` size | 256 KiB | `IDOP-LIMIT-010` |
| manifest JSON depth / node count | 32 / 4 096 | `IDOP-LIMIT-011` |
| state (`storage/**`) during a session | 8 MiB, 1 024 files | `IDOP-STORAGE-QUOTA` |

A Producer MUST NOT write a package that exceeds them. Future versions MAY raise
these limits; they will not lower them.

## 7. Manifest

### 7.1 Syntax

`idop.json` MUST be a UTF-8 JSON object [RFC 8259] that validates against the
schema in Annex A (`https://idoplabs.com/spec/idop/1.0/schemas/manifest.json`).
Duplicate object keys MUST cause refusal (`IDOP-MANIFEST-001`). Members not
defined by the schema MUST cause refusal (`IDOP-MANIFEST-003`): the place for
anything else is `extensions`.

### 7.2 Members

| Member | Required | Value |
| --- | --- | --- |
| `format` | yes | exactly `"https://idoplabs.com/ns/idop/package"` |
| `formatVersion` | yes | exactly `"1.0"` |
| `runtimeApiVersion` | yes | exactly `"1.0"` |
| `entryPoint` | yes | path of an existing `.html` entry under `code/` |
| `application` | yes | object, §7.3 |
| `document` | yes | object, §7.4 |
| `state` | yes | `{ "schemaVersion": <SemVer> }` — version of the document's own state format |
| `requiredFeatures` | yes | array of unique strings; 1.0 defines only `idop.core-storage-v1` |
| `optionalFeatures` | yes | array of unique strings |
| `requiredCapabilities` | yes | array of capabilities, §12.1 |
| `optionalCapabilities` | yes | array of capabilities, §12.1 |
| `environmentBindings` | yes | array, §12.3 |
| `credentialBindings` | yes | array, §12.2 |
| `extensions` | yes | object; keys are reverse-domain namespaces |

A Reader MUST refuse a package whose `requiredFeatures` contains a value it does
not implement (`IDOP-UNSUPPORTED-002`) and MUST ignore unknown
`optionalFeatures`.

### 7.3 `application`

| Member | Required | Value |
| --- | --- | --- |
| `id` | yes | lowercase reverse-domain identifier, ≤ 160 bytes (`com.example.board`) |
| `version` | yes | Semantic Versioning 2.0.0 |
| `title` | yes | 1–120 characters |
| `description` | no | ≤ 1 000 characters |
| `author` | no | ≤ 160 characters |
| `icon` | no | path of an entry under `resources/` |

**All `application` values are self-declared.** Nothing in 1.0 proves who wrote
a package. A Reader MUST NOT present them as verified and SHOULD label them as
supplied by the document.

### 7.4 `document`

| Member | Required | Value |
| --- | --- | --- |
| `id` | yes | UUID identifying the document across revisions |
| `revisionId` | yes | UUID of this revision |
| `parentRevisionId` | yes | UUID of the previous revision, or `null` for the first |
| `createdAt` | yes | RFC 3339 timestamp in UTC (`Z`) |
| `modifiedAt` | yes | RFC 3339 timestamp in UTC, not earlier than `createdAt` |

`parentRevisionId` MUST NOT equal `revisionId` (`IDOP-MANIFEST-014`). A document
identity grants nothing: it is not a security principal.

### 7.5 Container agreement

The `mimetype` entry and the manifest MUST agree: a manifest with
`formatVersion` `"1.0"` MUST be in a container whose `mimetype` is
`application/vnd.idop+zip` (`IDOP-PROFILE-005`).

## 8. Executable boundary

1. Only entries under `code/` are executable. An entry under `resources/` or
   `storage/` with the extension `.html`, `.htm`, `.js`, `.mjs`, `.css` or
   `.wasm` MUST cause refusal (`IDOP-POLICY-001`).
2. Entries under `code/` MUST be UTF-8 text with the extension `.html`, `.js`,
   `.mjs`, `.css` or `.json` (`IDOP-POLICY-002`, `IDOP-POLICY-003`).
3. HTML under `code/` MUST NOT contain inline script (every `<script>` has a
   `src` and an empty body), and MUST NOT contain `<iframe>`, `<object>`,
   `<embed>`, `<form>`, `<base>` or remote URLs (`IDOP-POLICY-011`,
   `IDOP-POLICY-013`, `IDOP-POLICY-014`).
4. CSS under `code/` MUST NOT contain remote URLs or `@import`
   (`IDOP-POLICY-012`).
5. Document code MUST NOT use dynamic evaluation (`eval`, `new Function`),
   `javascript:` URLs, WebAssembly, workers, service workers or `window.open`
   (`IDOP-POLICY-010`).

These checks are static and therefore **defence in depth, not the boundary**.
The boundary is the Reader's sandbox (§9.3, §17.1): a Reader MUST enforce
isolation whether or not the static checks would have caught a construct.

## 9. Reader processing model

### 9.1 Validation before execution

A Reader MUST perform these steps, in order, and MUST NOT expose any entry to
document code, or run any document code, until all of them succeed:

1. Check the file size against §6.
2. Parse the ZIP structure independently of any ZIP library — end of central
   directory, central directory, local headers — and enforce §4.1.
3. Check the first entry and its content against §4.2 (`IDOP-PROFILE-002`).
4. Validate every entry name against §4.3 and §5.
5. Decompress every entry into a bounded buffer, enforcing per-entry, aggregate
   and ratio limits and verifying every CRC-32 and size.
6. Parse and validate `idop.json` against §7, including §7.5.
7. Enforce the executable boundary, §8.
8. Resolve required features, configuration and capabilities (§12.4).

A failure at any step MUST cause the whole package to be refused with a stable
error code (§16). There is no "best effort" mode: a Reader MUST NOT run a
package that failed validation, and MUST NOT run part of one.

### 9.2 Loading

The Reader loads `entryPoint` and serves the other entries under `code/` and
`resources/` to the document by path. It MUST NOT serve `storage/` as files:
state is reached only through the Runtime API (§11). Entries are served with a
media type derived from their extension; unknown extensions MUST be served as
`application/octet-stream`.

### 9.3 Isolation

Document code MUST run in an execution context that:

- cannot read or modify the Reader's own interface, storage, cookies or
  credentials;
- has **no direct network access**: no `fetch`, XHR, WebSocket, beacon, form
  submission, navigation or resource load to any remote origin;
- cannot open windows or frames other than those the Reader itself provides;
- reaches the Reader only through the Runtime API transport the Reader injects.

In a web browser the RECOMMENDED realisation is an `<iframe>` with
`sandbox="allow-scripts"` and **without** `allow-same-origin` (an opaque
origin), whose content security policy is
`default-src 'none'` plus the sources the Reader itself needs to serve package
entries. A Reader MUST NOT grant `allow-same-origin` to document code.

### 9.4 One document, one context

Each session MUST have its own context and its own Runtime API channel. One
session MUST NOT be able to reach another session's channel, state or
capabilities.

## 10. Storage, Save and revisions

### 10.1 Overlay

On opening, the Reader copies `storage/**` into an in-memory overlay. Reads and
lists see the overlay; writes and deletes change only the overlay. The package
file is not modified until the user saves.

Paths given to the storage API are relative to `storage/` and MUST satisfy §5.
A path that names the `storage/` prefix itself, or escapes it, MUST be refused
(`IDOP-PATH-020`).

### 10.2 Durability states

A Reader SHOULD show the user which of three states a document is in:

| State | Meaning |
| --- | --- |
| `memory` | the document changed its own model but has not written to storage |
| `session` | the overlay changed; the file has not |
| `file` | the latest overlay has been saved into the file |

### 10.3 Save

Saving produces a **new revision**:

1. If the Reader saves into the file it opened, it MUST first verify that the
   file has not changed since it was opened (by content digest). If it has, it
   MUST NOT overwrite it (`IDOP-SAVE-CONFLICT`) and SHOULD offer "Save as".
2. The new package MUST be identical to the old one except for `storage/**` and
   `idop.json`.
3. In `idop.json`, `parentRevisionId` becomes the previous `revisionId`, a new
   UUID becomes `revisionId`, and `modifiedAt` becomes the current UTC time.
   `document.id` is unchanged.
4. The new package MUST be fully validated (§9.1) before it replaces the old
   file, and the replacement SHOULD be atomic.

"Save as" follows the same steps without the check in 1. There is no autosave
into the package.

## 11. Runtime API

### 11.1 Client

Before document code runs, the Reader MUST install a frozen object at
`globalThis.idop`. Document code MUST use it and MUST NOT infer any endpoint
from `location`. The transport behind it is the Reader's choice (in a browser,
a `MessagePort` per session is RECOMMENDED) and is not part of this contract.

Every method returns a Promise. A failure rejects with an `Error` whose `code`
property is a stable error code (§16). **Documents MUST branch on `code` and MUST
NOT parse `message`**, which is a diagnostic and may be localised or changed.

### 11.2 Methods

| Method | Parameters | Result |
| --- | --- | --- |
| `idop.runtime.getInfo()` | — | `{ runtimeApiVersion: "1.0", availableCapabilities: string[] }` |
| `idop.storage.read(path)` | storage-relative path | `{ text }` — UTF-8 content (`IDOP-STORAGE-NOT-FOUND`, `IDOP-STORAGE-UTF8`) |
| `idop.storage.write(path, text)` | path, UTF-8 string | `{ written: true, ... }` (`IDOP-STORAGE-QUOTA`) |
| `idop.storage.delete(path)` | path | `{ deleted: true, ... }` |
| `idop.storage.list()` | — | `{ paths: string[] }` |
| `idop.storage.transaction(operations)` | `[{ op: "write", path, data } \| { op: "delete", path }]` | `{ committed: true, ... }` — all or nothing |
| `idop.env.get(id)` | environment binding id | `{ value }` (`IDOP-ENV-DENIED`, `IDOP-ENV-MISSING`) |
| `idop.network.request(request)` | §11.3 | `{ status, headers, body }` |
| `idop.ui.requestSave()` | — | asks the Reader to show its own Save control; the decision stays with the user |
| `idop.ui.requestConfiguration()` | — | asks the Reader to show its configuration interface |
| `idop.host.getInputFile()` | — | `{ name, extension, size, bytes }` when the document was opened to display a file (§11.4), else `IDOP-HOST-NO-INPUT` |
| `idop.host.putOutputFile(bytes)` | `Uint8Array` | hands an edited copy of that file back to the Reader; writes nothing |

There is deliberately **no** method that returns a secret, enumerates
environment variables, or exports content to an arbitrary destination.

### 11.3 `network.request`

```text
{ capability: string, url: string, method?: string,
  headers?: { [name]: string }, body?: string, timeoutMs?: number }
```

The Reader performs the request on the document's behalf, subject to §12. The
response is `{ status, headers, body }`, where `headers` contains only a
Reader-defined allowlist of response headers and `body` is text. Errors:
`IDOP-NETWORK-DENIED`, `IDOP-NETWORK-ORIGIN-DENIED`,
`IDOP-NETWORK-METHOD-DENIED`, `IDOP-NETWORK-TIMEOUT`,
`IDOP-NETWORK-REQUEST-TOO-LARGE`, `IDOP-NETWORK-RESPONSE-TOO-LARGE`,
`IDOP-NETWORK-CONCURRENCY`, `IDOP-NETWORK-BUDGET-EXHAUSTED`,
`IDOP-CREDENTIAL-MISSING`, `IDOP-CREDENTIAL-ORIGIN-DENIED`. A non-2xx response
from the remote service is **not** an error: it is returned with its status.

### 11.4 Viewers *(informative)*

A Reader may let the user open an ordinary file (for example a `.docx`) with an
IDOP document chosen as its viewer. The viewer receives exactly that one file
through `host.getInputFile()` and nothing else from the user's storage.

## 12. Capabilities, credentials and permissions

### 12.1 Network capabilities

A capability object has these members:

| Member | Value |
| --- | --- |
| `id` | lowercase kebab-case, unique in the package |
| `type` | `"network"` (the only type in 1.0) |
| `origins` | 1–16 exact HTTPS origins, without path, query or fragment |
| `methods` | non-empty subset of `GET`, `POST`, `PUT`, `PATCH`, `DELETE` |
| `credentialBinding` | optional id of a credential binding |
| `purpose` | 1–240 characters shown to the user |

`requiredCapabilities` are needed for the document to work at all;
`optionalCapabilities` may be declined while the document still runs.

### 12.2 Credential bindings

A credential binding is `{ id, required, description? }`. **It names no service.**
A binding is satisfied by a credential the Reader holds whose allowed origins —
set by the user, never by the document — cover every origin of every capability
that references the binding.

- Every binding MUST be referenced by at least one capability
  (`IDOP-MANIFEST-026`).
- A binding MUST NOT contain a `provider` member (`IDOP-MANIFEST-025`).
- A Reader MUST NOT reveal a credential value to document code, write it into
  `storage/**`, a log, an export or a response.
- A Reader MUST hold, for each credential, its own list of allowed origins, and
  MUST NOT send the credential to any other origin. The manifest can only narrow
  where a credential is used; it can never widen it
  (`IDOP-CREDENTIAL-ORIGIN-DENIED`).
- A Reader MUST NOT fall back to an unauthenticated request when a credential is
  refused.

A Reader MAY offer well-known services as presets in its own settings. Such a
preset is matched to a binding by origin, exactly like any other credential.

### 12.3 Environment bindings

An environment binding is `{ id, variable, required, description? }`, where
`variable` matches `^[A-Z][A-Z0-9_]*$`. Its value is non-secret configuration
held by the Reader and returned by `env.get(id)` only for declared bindings.
Values are, by definition, readable by document code; a Reader MUST NOT offer an
environment binding as a place to keep secrets.

### 12.4 Preflight and consent

Between validation and execution the Reader MUST:

1. resolve required credential and environment bindings (asking the user to
   provide them, or refusing the document if they are not provided);
2. review every capability against what the Reader will actually permit, and
   resolve conflicts itself — a capability whose credential may not reach its
   origins MUST be shown as refused, never offered; a *required* capability in
   that state MUST prevent the document from running;
3. only then ask the user. The prompt MUST state, for each capability, which
   credential would travel to which origins and for what declared purpose. The
   user MAY cancel, allow once, or allow for this version.

"Allow for this version" MUST be keyed by a digest over the application id,
application version, a digest of all `code/**` entries, and the canonical
capability declarations. A change to code or to declared capabilities
invalidates it; a change to `storage/**` alone does not.

### 12.5 Broker rules

For each `network.request` the Reader MUST:

- refuse a capability that was not granted (`IDOP-NETWORK-DENIED`);
- refuse a URL whose origin is not exactly one of the capability's origins
  (`IDOP-NETWORK-ORIGIN-DENIED`), and a method it does not list
  (`IDOP-NETWORK-METHOD-DENIED`);
- remove `Authorization`, `Cookie`, `Proxy-Authorization` and similar headers the
  document supplies, and inject a credential only after all checks pass;
- not follow redirects;
- bound request size, response size, time and concurrency;
- enforce a finite **request budget per session**
  (`IDOP-NETWORK-BUDGET-EXHAUSTED`), so an approved document cannot spend a
  user's paid API credit in a loop. A request refused before it reaches the
  network does not consume the budget.

## 13. Portable Items (extension)

A document whose state holds user-visible objects (a note, a card, a quiz) MAY
declare under `extensions["idop.portable-items"]` how they are indexed, so that a
Reader can export one item as a new, valid package with a new `document.id`. The
declaration, the index format and their errors (`IDOP-ITEMS-*`) are defined in
*IDOP Workspace Containers 1.0*, §5, and its schema
`https://idoplabs.com/spec/idop/1.0/schemas/portable-items.json`. The Reader, not
the document, derives the exported package; there is no export API.

## 14. Bundles and containers

Directories that group documents — a **workspace** (`idop.workspace.json`), a
**project** (`idop.project.json`) — and the `.idopbundle` transport that carries
a workspace, folder or project as one file are defined in *IDOP Workspace
Containers 1.0*. A bundle is a ZIP archive under the same profile as §4.1, whose
first entry is a Stored `mimetype` containing `application/vnd.idop.bundle+zip`,
followed by `idop.bundle.json` and `content/**`. A Reader that implements
packages need not implement containers.

## 15. Versioning and compatibility

- `formatVersion` is `MAJOR.MINOR`. Minor versions of 1.x only **add**:
  optional members, optional features, new capability types gated by a feature
  name, new error codes. A 1.0 Reader that meets a `1.x` package with x > 0 MAY
  open it when everything it requires is understood, and MUST refuse it
  otherwise.
- A new major version changes the media type or the meaning of an existing
  member, and a Reader MUST refuse a major version it does not implement
  (`IDOP-UNSUPPORTED-001`).
- Error codes, once published, keep their meaning forever.
- Pre-release drafts (`0.1-draft`, `0.2-draft`, media type
  `application/vnd.idop.foundation+zip`) are not part of this specification. A
  Reader MAY continue to open them; a Producer MUST NOT write them.

## 16. Error codes

Every refusal and every Runtime API failure carries a stable code. Codes are
grouped by prefix; the complete registry, with one line per code, is published
with the conformance suite (Annex B).

| Prefix | Raised by |
| --- | --- |
| `IDOP-ZIP-*` | ZIP structure (§4.1) |
| `IDOP-PROFILE-*` | container identification and agreement (§4.2, §7.5) |
| `IDOP-PATH-*` | entry names and storage paths (§4.3, §5) |
| `IDOP-LIMIT-*` | limits (§6) |
| `IDOP-MANIFEST-*` | manifest (§7, §12) |
| `IDOP-UNSUPPORTED-*` | versions and features (§7.2, §15) |
| `IDOP-POLICY-*` | executable boundary (§8) |
| `IDOP-STORAGE-*`, `IDOP-SAVE-*` | storage and Save (§10) |
| `IDOP-NETWORK-*`, `IDOP-CREDENTIAL-*`, `IDOP-ENV-*` | broker and bindings (§11–12) |
| `IDOP-RUNTIME-*`, `IDOP-HOST-*`, `IDOP-SESSION-*`, `IDOP-BRIDGE-*` | Runtime API transport |
| `IDOP-ITEMS-*`, `IDOP-WS-*`, `IDOP-PROJECT-*`, `IDOP-BUNDLE-*` | containers and Portable Items |

A Reader SHOULD explain each code to the user in the user's language. The code
itself MUST NOT be localised.

## 17. Security considerations

### 17.1 Active content

An IDOP package **contains executable code**. This is its purpose, and the whole
of this specification is organised around it:

- Code runs only after the complete package has been validated (§9.1), so a
  malformed or truncated file never runs at all.
- Code runs in an isolated context with no network access, no access to the
  Reader, and no access to other documents (§9.3, §9.4).
- Network access exists only as a capability the user grants, to exact origins,
  through a broker that injects credentials the document cannot read (§12).
- A document cannot obtain a secret: there is no API that returns one (§11.2).
- The static checks of §8 are an additional layer; the sandbox is the boundary.

Residual risks that this specification does not remove: defects in the browser
or engine that hosts the sandbox; side channels; denial of service through CPU
or memory use (there is no portable way to bound a sandboxed context's CPU);
and **social engineering** — a document can display anything, including a
convincing imitation of a login form. Readers SHOULD visibly distinguish
document content from their own interface and SHOULD warn before running a
document received from someone else that requests network access.

### 17.2 Container attacks

The ZIP profile (§4.1), path rules (§5) and limits (§6) exist to defeat known
attacks on archive formats: path traversal ("zip slip"), entry-name collisions
across file systems, decompression bombs, polyglot and self-extracting files,
divergent local and central headers, and ZIP64 parser differentials. A Reader
MUST apply them before any extraction or execution.

### 17.3 Integrity and authenticity

1.0 provides **no authenticity**. `application` metadata is self-declared, and
the digest of `code/**` is a fingerprint used for consent (§12.4), not a
signature. Anyone can produce a package claiming any title or author. A Reader
MUST NOT present a package as coming from a particular publisher. Signatures are
reserved (`_idop/signatures/`) for a later version.

### 17.4 Confidentiality

A package is not encrypted. Anything in `storage/**` is readable by anyone who
has the file. Encryption is reserved (`_idop/encryption/`) for a later version.

### 17.5 Export

A Reader cannot prevent the user from keeping a copy of a document they can
open. "Read-only" in a Reader's interface is not access control and MUST NOT be
described as such.

## 18. Privacy considerations

- A package carries its state. Users SHOULD be told that sending a document
  sends its saved content.
- Credentials and environment values are Reader state and never travel with the
  package (§12.2, §12.3); a recipient must configure their own.
- A document cannot contact any server unless the user grants a capability; the
  prompt names each origin, so the user knows where data could go.
- Timestamps in `document` reveal when a document was created and last saved.
  Producers MAY round them.

## 19. Accessibility considerations

Documents are web content and SHOULD meet WCAG 2.2 level AA. A Reader SHOULD
make its own interface — prompts, errors, the document frame's title —
accessible, and SHOULD give the document frame an accessible name derived from
the document title while labelling it as document content.

## 20. IANA considerations

This specification requests registration of two media types in the vendor tree
[RFC 6838]. The registration templates are reproduced in
`docs/registrations/` of the reference repository.

**`application/vnd.idop+zip`**

- Required parameters: none. Optional parameters: none.
- Encoding considerations: binary.
- Security considerations: §17 of this specification.
- Interoperability considerations: §4, §15.
- Published specification: this document.
- Applications that use this media type: IDOP Cloud (`https://cloud.idoplabs.com`),
  the `idop` command-line tool, and other IDOP Readers.
- Fragment identifier considerations: none defined.
- Magic number: `50 4B 03 04` at offset 0, the string `mimetype` at offset 30,
  and the string `application/vnd.idop+zip` at offset 38.
- File extension: `.idop`. Macintosh file type code: none.
- Uniform Type Identifier (informative): `com.idoplabs.idop`, conforming to
  `public.zip-archive`.

**`application/vnd.idop.bundle+zip`** — as above, with the bundle string at
offset 38 and the extension `.idopbundle`.

## 21. References

### Normative

- [APPNOTE] PKWARE, *.ZIP File Format Specification*, version 6.3.10.
- [RFC 2119] Bradner, *Key words for use in RFCs to Indicate Requirement Levels*.
- [RFC 3339] Klyne, Newman, *Date and Time on the Internet: Timestamps*.
- [RFC 4122/9562] *Universally Unique IDentifiers (UUIDs)*.
- [RFC 6454] Barth, *The Web Origin Concept*.
- [RFC 6838] Freed, Klensin, Hansen, *Media Type Specifications and Registration Procedures*.
- [RFC 6839] Hansen, Melnikov, *Additional Media Type Structured Syntax Suffixes* (`+zip`).
- [RFC 8174] Leiba, *Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words*.
- [RFC 8259] Bray, *The JavaScript Object Notation (JSON) Data Interchange Format*.
- [SemVer] *Semantic Versioning 2.0.0*.
- [UAX15] Unicode Standard Annex #15, *Unicode Normalization Forms*.
- [JSON Schema] *JSON Schema 2020-12*.

### Informative

- OASIS, *Open Document Format for Office Applications*, Part 3: Packages.
- W3C, *EPUB 3.3*, Open Container Format.
- W3C, *Content Security Policy Level 3*; WHATWG, *HTML* (`iframe` sandboxing).
- W3C, *Web Content Accessibility Guidelines 2.2*.

---

## Annex A — JSON Schemas (normative)

| Schema | `$id` |
| --- | --- |
| Manifest | `https://idoplabs.com/spec/idop/1.0/schemas/manifest.json` |
| Workspace marker | `https://idoplabs.com/spec/idop/1.0/schemas/workspace.json` |
| Project marker | `https://idoplabs.com/spec/idop/1.0/schemas/project.json` |
| Bundle manifest | `https://idoplabs.com/spec/idop/1.0/schemas/bundle.json` |
| Portable Items | `https://idoplabs.com/spec/idop/1.0/schemas/portable-items.json` |

The schemas also describe the pre-release drafts so that one validator can
read both; for a 1.0 manifest only the `formatVersion: "1.0"` branch applies.

## Annex B — Conformance suite (normative for claims of conformance)

A Reader or Producer may claim conformance to IDOP 1.0 when it passes the IDOP
1.0 conformance suite: a set of valid packages that MUST open, invalid packages
that MUST be refused with the listed error code, and round-trip cases for Save.
The reference implementation (the `idop` CLI and the IDOP Cloud Reader, which
share one validator) passes it.

## Annex C — Minimal example *(informative)*

`mimetype`:

```text
application/vnd.idop+zip
```

`idop.json`:

```json
{
  "format": "https://idoplabs.com/ns/idop/package",
  "formatVersion": "1.0",
  "runtimeApiVersion": "1.0",
  "entryPoint": "code/index.html",
  "application": { "id": "com.example.counter", "version": "1.0.0", "title": "Counter" },
  "document": {
    "id": "4df56319-e383-4ae8-a519-faa7f4866f90",
    "revisionId": "2e43a209-0a66-4f54-b545-dae25d3b89d0",
    "parentRevisionId": null,
    "createdAt": "2026-10-01T00:00:00Z",
    "modifiedAt": "2026-10-01T00:00:00Z"
  },
  "state": { "schemaVersion": "1.0.0" },
  "requiredFeatures": ["idop.core-storage-v1"],
  "optionalFeatures": [],
  "requiredCapabilities": [],
  "optionalCapabilities": [],
  "environmentBindings": [],
  "credentialBindings": [],
  "extensions": {}
}
```

`code/index.html`:

```html
<!doctype html>
<html lang="en">
  <head><meta charset="utf-8"><title>Counter</title><script type="module" src="app.js"></script></head>
  <body><button id="add">0</button></body>
</html>
```

`code/app.js`:

```js
const button = document.querySelector('#add');
let count = 0;
try {
  count = JSON.parse((await idop.storage.read('count.json')).text).count;
} catch (error) {
  if (error.code !== 'IDOP-STORAGE-NOT-FOUND') throw error;
}
button.textContent = String(count);
button.addEventListener('click', async () => {
  count += 1;
  button.textContent = String(count);
  await idop.storage.write('count.json', JSON.stringify({ count }));
});
```

A document that needs an external API adds, for example:

```json
"requiredCapabilities": [{
  "id": "ai", "type": "network", "origins": ["https://api.example.com"],
  "methods": ["POST"], "credentialBinding": "api-key", "purpose": "Generate answers"
}],
"credentialBindings": [{ "id": "api-key", "required": true, "description": "Key for api.example.com" }]
```

## Annex D — Changes from the pre-release drafts *(informative)*

| Draft | 1.0 |
| --- | --- |
| media type `application/vnd.idop.foundation+zip` | `application/vnd.idop+zip` |
| `format: "urn:idop:package"` | `https://idoplabs.com/ns/idop/package` |
| two profiles, `0.1-draft` and `0.2-draft` | one profile, `1.0`; a document without capabilities is the former 0.1 |
| credential binding names a provider (`openrouter`, `bearer`) | names no service; matched by origin (§12.2) |
| schemas under `idop.foundation` | schemas under `idoplabs.com/spec/idop/1.0/` |
| — | reserved `_idop/signatures/`, `_idop/thumbnail.png`, `_idop/encryption/` |
