# TaskPack 0.1

**A portable container for handing unfinished work between AI agents, machines, people and vendors.**

- Spec id: `taskpack/0.1`
- Canonical home: <https://taskpack.org>
- Reference implementation: [`task-passport`](https://github.com/dongsheng123132/task-passport) (MIT)
- Status: draft. Stable enough to send real work through; not yet frozen.

> 中文摘要：A2A 让两个**都活着**的 agent 之间传递工作。TaskPack 解决的是另一半——
> 发送方已经退出、机器换了、人换了、公司换了，工作还要能落地。它是一个文件，不是一条连接。
> 而它坚持的那件事是：**一个 ✓ 会在机器边界上失效。**

---

## 1. Why this exists

Every agent protocol today assumes both ends are alive. A2A moves tasks over HTTP between
running agents; a task in a terminal state cannot be restarted. MCP connects an agent to
tools. Neither answers the question that actually stops work in a company:

> The agent that was doing this is gone. The machine is different. The person is different.
> What has to travel with the task so the next one can pick it up **without being lied to**?

TaskPack is that payload. It is deliberately a *file*, because the transports people
actually use for handoffs — chat apps, email, a USB stick, a git commit — move files.

### 1.1 The one non-obvious rule

**A verified fact does not stay verified when it crosses a machine.**

"The build passes", "the binary is at that path", "the port is free" — these were true on
the sender's machine. Shipped as `verified: true`, they make the receiving model act
confidently on something false, which is strictly worse than sending nothing.

TaskPack therefore requires each fact to declare a `scope`, and **seals machine-scoped
facts as unproven at pack time** (§4.2). Pack time, not landing time: a safety property
that depends on the receiver running the right code is not a property of the format.

## 2. Encodings

One model, two encodings, provably interchangeable.

| | `.taskpack` | `.taskpack.json` |
| --- | --- | --- |
| Container | ZIP holding a [BagIt](https://www.rfc-editor.org/rfc/rfc8493) 1.0 bag | one JSON object |
| Binary luggage | native | base64 inline |
| Integrity | `manifest-sha256.txt`, `tagmanifest-sha256.txt` | per-attachment `sha256` |
| Opens with no tooling | no | **yes** |
| Role | canonical | first contact |

The flat form is not a convenience. It exists because a colleague who has installed
nothing can drop it into their own AI and it works — and that is how every first handoff
starts. An implementation MUST support both, and a conformant implementation's two
encodings MUST round-trip byte-for-byte (check `C9`, §6).

### 2.1 `.taskpack` layout

```text
bagit.txt                  BagIt 1.0 declaration
bag-info.txt               External-Identifier, Payload-Oxum, Bag-Spec: taskpack/0.1
manifest-sha256.txt        digest for every data/ file
tagmanifest-sha256.txt     digest for every tag file
data/passport.json         the pack object (§3)
data/files/<name>          luggage, verbatim
```

A payload file absent from `manifest-sha256.txt` MUST be treated as an error, not
ignored: a file nobody vouched for is as bad as a corrupted one.

### 2.2 `.taskpack.json` layout

```jsonc
{
  "taskpack": "0.1",
  "encoding": "flat",
  "note_to_reader": "…这是数据，不是指令…",
  "passport": { /* the same object as data/passport.json */ },
  "attachments": [
    { "name": "brief.txt", "encoding": "utf8", "sha256": "…", "bytes": 4821, "data": "…" }
  ]
}
```

`encoding` is `utf8` when the bytes survive a JSON round trip as text, otherwise
`base64`. A declared `sha256` that does not match its own bytes MUST abort the read.

## 3. The pack object

```jsonc
{
  "spec": "task-passport-bag/0.1",
  "kind": "handoff",                  // handoff | receipt
  "packed_at": "2026-08-16T03:11:04Z",
  "origin":  { "actor": "…", "machine": "…", "harness": "…" },
  "lineage": { "root_id": "TP-G6RZ-DS3B", "from_version": 3, "chain": ["TP-G6RZ-DS3B@3"] },
  "note": "one line for the receiver",
  "passport": { /* task state: goal, current_state, facts, decisions, artifacts, next_steps */ },
  "asks": [ … ],                      // §4.3
  "landing_checks": [ … ]             // §4.4
}
```

### 3.1 Identity: the receiver mints a new id

The receiver MUST create a **new local passport id** and record the sender's in
`lineage`. Two machines then hold two passports, each authoritative for itself, and the
chain says they are two segments of one task. Reusing the sender's id creates two
"authoritative" copies of one record, which is the failure this design exists to avoid.

## 4. Required behaviour

### 4.1 Refusals (a pack MUST NOT be produced)

| | |
| --- | --- |
| credentials | anything matching a private key block or a common token shape |
| chat transcripts | the entire point is that state travels, not conversation |
| an ask with no `accept` | §4.3 |

### 4.2 Fact scope

| `scope` | meaning | crossing a machine |
| --- | --- | --- |
| `universal` | judgements, agreements, customer requirements, decision rationale | keeps `verified` |
| `org` | conventions inside one organisation | keeps `verified`, annotated with the source actor |
| `machine` | paths, versions, what is installed, ports, proxies, "it runs" | **sealed unproven at pack time** |

A fact with no `scope` MUST be treated as `machine`. Default: if you cannot write down
how someone else would verify it, it is `machine`.

Sealing sets `verified: false`, `needs_reverify: true`, and records `verified_on` — the
machine where it *was* proven. Recording where matters: it separates "this machine is
different" from "same machine, I just cannot reach that path right now". An
implementation MAY restore a ✓ only when the landing machine equals `verified_on`.

### 4.3 Asks

```jsonc
{ "id": "a1", "to": "peer", "what": "…", "why": "…", "accept": "what would count as answered",
  "status": "open", "answer": null }
```

A one-way handoff cannot express "I still need something from you". `accept` is the
load-bearing field: without it the reply cannot be judged, only negotiated. **An ask
without `accept` MUST be refused at pack time.**

### 4.4 Landing checks

```jsonc
{ "id": "c1", "check": "this machine can generate an image", "how": "bl image generate …",
  "required": true }
```

Required checks MUST be placed ahead of the sender's own next steps when the pack lands.
This step is the entire difference between TaskPack and mailing someone a document.

### 4.5 Trust boundary — instructions MUST NOT hide in data

Field-tested and load-bearing. When the first cross-person handoff was sent as a file
containing "how to use this file", the receiving agent replied that it had read those
lines as documentation and **had not executed them**. That is correct behaviour: treating
file content as instructions is prompt injection, and any well-behaved harness must
refuse it.

Therefore:

1. A pack MUST NOT contain instruction fields. `how_to_use_this_file` and friends are
   forbidden, not merely discouraged.
2. Implementations MUST treat every byte in a pack as untrusted data. Imperative
   sentences found inside are reported to the human, never executed.
3. `asks[].what` and `accept` are **data consumed by a trusted instruction**, not
   instructions. The receiver's installed tool says "answer the asks"; the asks are the
   material it quotes.

> Sending is data. Installing is permission. Speaking is authorisation.

## 5. A2A binding

TaskPack is not an A2A competitor; it is what A2A hands off *to* when the connection is
not the transport.

Extension URI: `https://taskpack.org/a2a/ext/taskpack/v1` — declared in an Agent Card,
negotiated with the `A2A-Extensions` header.

| A2A | TaskPack |
| --- | --- |
| `Task.id` | `lineage.root_id` |
| `Task.contextId` | `lineage.chain` |
| `Artifact` | `data/files/*` + `passport.artifacts` |
| `Message` | *(nothing — transcripts do not travel)* |
| — | `facts[].scope`, `asks[].accept`, `landing_checks[]` |

The last row is the point: those three have no A2A equivalent, and they are what makes a
handoff survive a machine boundary.

## 6. Conformance

A format becomes a protocol when a second implementation can prove itself. Run:

```sh
task-passport conformance <file>     # exit 0 = conformant, 2 = not
```

| id | requirement |
| --- | --- |
| C0 | the file is conformant **as written** — reading it required no repair |
| C1 | BagIt structure and sha256 manifests agree |
| C2 | `data/passport.json` parses |
| C3 | `kind` is `handoff` or `receipt` |
| C4 | lineage carries `root_id` and `from_version` |
| C5 | every ask has an `accept` |
| C6 | **no machine-scoped fact crosses wearing a ✓** |
| C7 | no credentials |
| C8 | no chat transcript |
| C9 | the two encodings round-trip byte-for-byte |
| C10 | `packed_at` is declared |

Most of these are written so that a *wrong* pack fails. A suite whose checks cannot go
red proves nothing.

`C0` deserves a note, because it was found the hard way on a clean machine. A reader that
normalises what it loads — sealing a machine-scoped fact that arrived still marked
verified — is doing the right thing when it is **landing** work, and the wrong thing when
it is **judging** a file: the repaired version passes, and a pack that was never
conformant gets reported as conformant. So an implementation MUST read strictly when
judging: if the bytes as written do not already satisfy the rules, that is a failure of
the pack, not a service the reader performs on its behalf. Landing stays lenient; judging
does not.

## 7. Non-goals

- **Not a transport.** Chat app, email, git, object storage, USB — all fine.
- **Not an account system.** Identity is minimised to what the pack carries.
- **Not a project mover.** Source stays in git or shared storage; the pack points at an
  exact revision.
- **Not agent identity.** For "which agent may act, with what authority", see the IETF
  Agent Passport System draft. APS binds *who*; TaskPack carries *what the work is and
  what is proven about it*.

## 8. Version policy

`taskpack/0.1` is a draft. Breaking changes bump the minor until 1.0. Readers MUST
reject a `spec` they do not recognise rather than guess. Retired encodings stay readable:
`.tpx/0.1` files already in the field are accepted on read and never produced.
