# PAC Why PAC exists, how it is designed, how the code on main is built, and which repository to open. # Start here **PAC is a planetary-scale networked autonomous computing system**: every person or organisation runs a node that acts for them, and the nodes find each other, negotiate, and compute together without handing over what they hold. This site is the map of the project. Each repository's README is the reference the map links into. Read in this order: * [Why](/why) -- the trends PAC follows from, and the hypothesis. * [Design](/design) -- what a node is meant to be: the terms, the core every node runs, and the kinds of plugin that extend it. * [Architecture](/architecture) -- what the code on `main` is, path by path, and what is not built yet. * [Repositories](/repos) -- which repository to open for what you want to do, and how to get them onto one machine. For agents: append `.md` to any page's URL for its markdown, or read [`llms-full.txt`](https://docs.projectpac.net/llms-full.txt) for the whole site in one file. # Why PAC **PAC is a planetary scale networked autonomous computing system** of agents with each PAC node on the network acting on behalf of individuals or organisations to extend their capabilities, harness and protect their data, and enable interactions/transactions across the network using multi-agent cryptographically secure joint computing techniques. **Small p in pAC** stands for **"personal"** meaning it works for every individual, small business, and highly personal to create self-sovereign agency and control at the node level **Capital P in PAC** stands for **"Planetary"** meaning it is a networked autonomous system designed to work at planetary scale ## Core trends and hypothesis 1. **There will be model abundance**. Multiple models — small and large, centralized and decentralized/on-device — all coexisting. Reasoning is no more centralized as special purpose models and on-device models are becoming accurate and cheaper. In addition, continual learning techniques will become commonplace enabling true intelligence abundance to take place. 2. **There will be task/labour abundance**. These are driven by AI agents, tools, and loops, taking AI beyond knowledge access towards complex multi-step task execution powered by multi-level reasoning. 3. **Creation will be abundant** as generative AI is eliminating the need for purpose specific approaches (code, UX, protocols, etc) to be replaced with meta/purpose-generic approaches. This means purpose specific software products/SaaS have little moat both for consumers and enterprises. 4. **Intelligent personalization on the edges will be the norm**. Consumer apps (super apps, special purpose apps, etc), as we know, will increasing lose relevance. Instead providers will offer agent-ready composable services with meta rules that will be dynamically composed by consumer's own agents generating necessary UX elements and workflows on-the-fly as required. 5. **Human attention and intention arbitrage will be reimagined**. Everything we know on the Internet is designed to assume humans are executing human intent, AI is flipping this on its head and will have massive implications on the way information is organized, curated, items are cataloged, how ads are working, and so on. 6. **Value-chains will be unbundled when agents become your new customer**! As intelligent agents can execute millions of times faster with billions of options evaluated, value-chains of today will get unbundled and rebundled having a topology of a fully decentralized network. This will move the true power to the edges of such decentralized networks - to consumers and providers. 7. **Cryptography will shape new forms of guarantees**. Generative AI, agentic execution, combined with cryptography will be the future of networked computing. Reasoning, agentic loops, and generative systems allow intelligent systems to, on-the-fly, generate negotiated outputs that can then be used to run deterministic sub-system tasks. The non-determinism of AI and determinism of traditional systems is powerful when combined. Cryptographic techniques are offering us various forms of guarantees - from access to proofs to executing multi-party privacy-preserving computations on a network. 8. **True path to diffusion**: While Digital Public Infrastructure (DPI) efforts gave ID, credentials, and data in the hands of billions of users and small businesses, AI is giving enhanced human capability to them. Combined, we unlock the core three elements - ACCESS, TRUST, CAPABILITY - to 8 Bn individuals and 300+ million small businesses to leverage the coming intelligent economy! 9. **Hence PAC**! In the near future, every individual and organisation will have their own personal autonomous computing system where agents breakdown and execute their intents unburdening them from mundane, low-value-add tasks. Such a system will manage their data security and in a privacy-preserving manner within their personal node, enabling them with advanced reasoning and agentic execution capabilities. Each such node will be enabled to discover, trust, interact, compute, and transact with any other similar node, and thus forming a planetary scale networked autonomous computing system. 10. **NETWORK IS THE AGI!** ## Inspiration WWW -> Multi-agent Web -> Autonomous Networked Systems. The Web has historically been designed primarily around humans accessing information and applications. Consumer platforms competed to help humans access information via curated feeds, curated catalogs, curated social connections, driving the rise of consumer apps/platforms and ad economy. The next evolution is a Web in which agents become active participants where agents will act on-behalf of humans/businesses to execute their intents. Agents will discover, communicate, establish mutual trust, collaborate and execute, rather than merely serving as interfaces for humans. The hypothesis is that a trillion agents, running on diverse hardware and using diverse capability models, will eventually be connected to each other. **These agents will form an intelligent web.** Rather than one large system doing everything, intelligence emerges from a network of many different agents and capabilities. The core difference is that in such a network, multiple agents will connect, negotiate, and co-author plans and securely execute the tasks in a highly privacy preserving manner while each node still deriving intelligence and achieving its goals. ## PAC AI is giving capability to the long tail. As agents move to the edge and become increasingly capable of execution, multiple models and heterogeneous hardware will coexist and connect. These agents can form an open intelligent network — beyond a multi-agent web — ultimately enabling autonomous networked joint computing. Under Project PAC, we are building an autonomous networked computing system. Join us. ## Where next * [Design](/design) -- what a node is meant to be. * [Architecture](/architecture) -- what is built today, and what is not. * [Repositories](/repos) -- which repository to open for what you want to do. # Design What PAC is meant to be, as a system: its concepts, the fixed core every node runs, and the kinds of plugin that extend it. Nothing on this page names a package, a path, a port or a number. What of this exists today, and where, is the [architecture](/architecture) page's job; why PAC exists at all is [why](/why). ## Terms * **Node** -- one running instance of PAC, acting for a single principal: a person, a group, a company, or another agent. The network is the set of such nodes. * **Peer** -- another node, as seen from this one. * **Identity** -- a keypair a node presents to peers, its id derived from the public key. A node holds one or more, and a peer cannot tell that two of them are the same node. * **Flow** -- an installable program that runs multi-step interactions with peers: it defines the messages, the state each side keeps, and the points where a model is invoked. * **Session** -- one run of a flow between nodes. * **Message** -- one signed envelope from node to node. * **Artifact** -- a versioned object two nodes negotiate over. Each change is a revision, signed by its author, and agreement is a mark on one revision. * **Authorization** -- permission from the principal for an action. Policy is one mechanism for expressing it in advance; what policy does not cover is asked of the principal and answered one action at a time. * **UX** -- a way for the principal to drive the node: a cli, a page, an MCP server, a chat surface. A client of the control API, not a plugin the node loads. * **Trace** -- the attributed record of what happened. * **Plugin** -- anything that plugs into the node. The **core plugins** specified below are what a node *is*: every node runs all of them. Everything a principal installs is a **non-core plugin**: the adapters, the flow services, and the flows. ## The shape of a node Everything a node runs is a plugin, and every kind has the same shape: a manifest declaring what it is and what it will request, reach into the node only through the core plugins' operations, one folder of its own, and a trace of what it did. What separates the kinds is what each one plugs into. Most non-core plugins are **adapters**: one outside system bound to one of the node's ports. What separates one group of adapters from another is the port -- which core plugin calls it, and what it is handed -- not whether it reaches outside the node, since all of them do. That is what lets implementations be swapped or run side by side, and it is why the groups are named for the core plugin whose port they plug into: a new group is a new port on a core plugin, and one that grows a port leaves the others as they were. | group | port on | for example | | --------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | data adapters | data | a **source** that ingests something the principal exported; an **operation** that applies a source somewhere sensitive -- a payment rail, an encrypted submission, a compiler | | intelligence adapters | intelligence | an **executor** a run runs on -- a cloud model, a local one, a personal agent's command line; a **tool** the model may call | | network adapters | network | a **transport** that moves envelopes -- the public internet, a private network, a local mesh | | discovery adapters | discovery | a whole mechanism for being found -- a directory, published claims, a distributed registry | | package sources | host | where a plugin's bytes come from -- a registry, a git remote, the checkouts beside a development node | A tool and an operation differ in who holds the material: a tool is invoked by the model with arguments the model chose; an operation is performed on the flow's behalf with material the flow named and never held. Three kinds are not adapters, because they back no core plugin: * **Flows** -- peer-facing behaviour, loaded by the host. These are the applications. * **Flow services** -- shared machinery the core does not provide, called by flows rather than spoken on the wire: a versioned artifact store; the suggest-and-agree rounds every negotiation runs; durable records a plugin keeps, with the caller derived rather than named so no plugin can open another's; an intent router that decides what a principal's sentence is asking for and starts it as a call on the chosen flow's own route, so the work is attributed to that flow and not to the router. * **UX** -- new ways for the principal to drive the node; clients of the control API. ``` the principal │ clients of the control API (not plugins) │ ────────────────────────────── control API ───────────────────────────── flows * │ ─────────── the core plugins' caller-aware operations ─────────── flow services * ┌───────────┐ ┌───────────┐ ┌──────────────┐ ┌────────────────┐ ┌──────────┐ │ network │ │ discovery │ │ intelligence │ │ data │ │ identity │ └─────┬─────┘ └─────┬─────┘ └──────┬───────┘ └───────┬────────┘ └──────────┘ │ │ │ │ transports * discovery executors * sources * │ adapters * tools * operations * │ │ │ │ ──────┼─────────────┼─────────────┼───────────────┼── trace ───────── │ │ │ │ peers directories, models, outside services (other nodes) published claims tools * = non-core plugins ``` The two core plugins not drawn as boxes are the lines: the host loads everything above the seam, and the api is the control API. Each plugin, core or not, owns one folder under the node's data directory and keeps everything of its own in it: its databases, its keys, its files, whatever a run of it produced. There is no shared store and no separate vault, and no plugin reaches into another's folder. What one holds is reached by asking it, which is what every operation below already is. ## Architectural style Two things are described here and they have different shapes. **A node** is a microkernel: a minimal core the node always runs, and all other behaviour in plugins that plug in through declared contracts. Flows are the applications of this kernel; the adapter kinds each back one core plugin, and the rest back none. Each seam also has the shape of ports and adapters: the core plugin defines the contract and owns everything above it, and a non-core plugin adapts one implementation to it, which is what lets implementations be swapped or run side by side without the core changing. **The network** has no kernel and no core, because it is not a program: it is the set of nodes, and every one of them is somebody else's. Its shape is peer-to-peer -- discovery introduces peers, and from then on nodes message each other directly, with nothing in the middle of the data path -- and what holds it together is the envelope rather than any shared installation. Two nodes interoperate because they speak it and run a flow of the same id and version, not because they were built alike, so nothing here says what a peer must be made of. Inside a node, flow execution is event-driven: a flow reacts to a delivered event -- a message arriving, a turn of one of its runs settling -- calls operations, and returns. Between nodes, delivery is asynchronous messaging with an outbox, retries, and deduplication on the receiving side. The same boundaries carry three principles. Separation of concerns: each core plugin owns one responsibility and states what it is not responsible for, so interpreting data, choosing peers, and moving bytes are never the same plugin's job. Least privilege: a plugin is authorized against the manifest it declares, reaches the node only through the core plugins' operations, and writes only its own folder. Complete mediation is the principle the seams are shaped for: every use of a data source is meant to pass through one check, and sensitive operations are performed on the flow's behalf, so raw contents, credentials, and keys never enter flow code and a check cannot be avoided by going around the plugin that enforces it. ## The core plugins Each is specified by what it owns, the operations it offers, and what it is not responsible for. ### Identity A node holds one node key and one or more network identities. The node key is exactly one: it signs the trace's checkpoints and names the node to its principal, and it need never reach a peer. A network identity is a keypair a peer sees, its id derived from the public key, and a node may hold several, so one node can present faces to different peers that they cannot link to each other. This plugin holds the keys and offers signing and verification to the others; the material never leaves it. A session binds to one identity when it opens and everything on it is signed by that one, which is what makes reputation accrue to an identity rather than to a node -- the cost of being unlinkable -- while the trace, being local, sees across all of them. * Provides: the node's id and the identities it can present; sign, as the node or as a named identity; verify a peer's signature against its id. * Not responsible for: deciding trust, or choosing which identity a session presents -- that is policy. Reputation is built above identity, on behaviour over time, per identity. ### Network Everything required to talk to other nodes, and nothing that moves bytes itself: the wire is a transport underneath, and this plugin is the layer above it. Every message is a signed envelope: sender and recipient, the sender's public key, the plugin id and version both ends run, the session, a unique message id, and the payload. The sender's id is derived from the key it carries, so any node can verify any envelope with no prior introduction, and first contact needs no handshake. Flows hand their outbound messages to this plugin and never touch the wire; it sees all inbound traffic, verifies it against the sender's identity, and routes each message to the plugin and session it belongs to. An envelope that fails verification, or names a plugin or version this node does not run, is rejected and traced. Delivery is at-least-once; ordering across retries is not guaranteed. * Provides: deliver an envelope; verify and route inbound; report delivery failure. * Not responsible for: choosing recipients (discovery) or interpreting payloads (the flow). ### Discovery How flows find work and the peers offering it. A flow states a need and gets back candidates, and a candidate is neither a node nor an endpoint on one: it is an **advertisement bound to a peer** -- the offer, and who is making it. What a node advertises is what it is willing to act on, so a flow finds an offer it can answer and receives the peer as part of the answer, rather than looking a node up and asking what it has. Nothing makes a *node* discoverable, and a node has no surface a peer can enumerate: what a peer can see is what the node chose to advertise. A peer is *named* by an identity and *located* by addresses that belong to its transports, which change and are refreshed on every query. There is no one way to be findable, so there is no one provider: an adapter implements a whole mechanism -- presence, advertising, withdrawal, query -- and a node runs as many as its principal installed. They are not ranked, and a need names the providers it is for, because mechanisms differ in exactly the ways a flow cares about: how narrowly a need can be asked, how fresh presence is, what a candidate carries, and who can see that the asking happened. One convention sits above every adapter: a flow that wants to be found advertises itself under one well-known kind, and a peer queries that kind to learn which live nodes speak it. The node advertises nothing on a flow's behalf, so a flow that says nothing is not findable, and that is the flow's choice. * Provides: publish an advertisement, and withdraw it; query for candidates matching a need, over the providers the need names. Presence is upkeep the plugin does, so a provider that has not heard from a node lately stops returning its advertisements. * Not responsible for: verifying or choosing a peer -- it returns candidates and the flow judges them. Nor for addressing. Nor for reconciling providers with each other: a candidate stays attributed to the mechanism that produced it. ### Intelligence The node's intelligence. A flow is mostly a state machine, and when a step calls for judgment -- what to propose, whether to accept, how to reply -- the flow starts a run and appends turns to it. A run is a conversation: its turns run in order, a later turn continues the session the first one opened, and a run stays open until the flow closes it. A run names the executor that will run its turns and carries two things the kernel stores and never reads: a payload for the executor, in whatever shape the executor defines, and metadata for the flow, echoed on every event about the run so a handler finds its own record. What the executor produces comes back to the flow exactly as produced, or as a structured error saying why it produced nothing. The kernel composes no prompt, parses no output, enforces no budget, and records no cost: what a turn means is the flow's to decide, and what a payload means is the executor's. A failed turn is not a failed run; the flow decides whether to append another. Turns execute in a process of their own that never opens the node's store, because the model's process is the least trusted thing in the node: it runs over material a flow chose to show it, under instructions nobody here reviewed. Which executor backs a run -- a cloud model, a local one, a personal agent -- is named per run, so a node is never tied to one model, or to models that are language models at all. Tools come from tool adapters, and a tool call with side effects is a gated action like any other. * Provides: start a run; append a turn; close a run; each settled turn comes back to the flow that started the run as one event, verbatim. * Not responsible for: deciding when to reason, what a turn is asked, or what to do with what came back. It runs only when a flow appends a turn, and it interprets nothing. ### Data **Designed, not built.** This is the specification the stand-ins are measured against; the [architecture](/architecture#not-built-yet) page says what stands in today. A collection of the principal's data sources -- exported data, credentials, payment instruments, skills. Sources are created and kept current by source adapters, and each source is owned by the adapter that writes it. Data policies are how authorization over sources is expressed and enforced: which flows and peers may learn what about a source, which uses are allowed on what terms, and what must be asked of the principal instead. When a flow needs a source applied somewhere -- staked into a joint computation, presented to an endpoint -- this plugin performs the operation on the flow's behalf, so contents never cross into flow code. That is the one verb: the flow names what to use and where it goes, and never holds what it named. * Provides: the source registry; source writes by owning adapters; the policy check every access passes through; execution of the operations that apply sources on a flow's behalf; the authorization queue, whose decisions come back as flow events the way settled turns do. * Not responsible for: interpreting the data. That is flow work. ### API One control API, and many faces on it: a cli, a page, an MCP server, a chat surface, or anything else that speaks to the node. This is where the principal watches what their representative is doing, manages what it holds, and grants or refuses authorization. The faces are UX plugins -- clients of this API rather than parts of it. Core routes are reserved by construction and everything a non-core plugin serves lives under a path of its own, so no plugin has a path with which to claim a core route; a plugin's route belongs to the plugin that declared it, so disabling the plugin withdraws the route too. * Provides: the control API itself -- watch, manage, decide -- and the seam a plugin's own routes are dispatched through. * Not responsible for: policy decisions themselves -- it renders them. Nor for what any one UX looks like. ### Host The core plugin that manages installed non-core plugins. The node boots it along with every other core plugin; the host then loads what the principal has installed and manages each one the way an OS manages processes: add, remove, enable, disable, kill. It does not sit between a flow and the rest of the node. A flow reaches the other core plugins directly through the caller-aware operations they themselves offer, and every one of those operations knows which plugin called it, checks policy, and leaves a trace. Because everything a flow registers goes through those operations, disabling it withdraws all of it, and a flow whose dependencies are missing waits rather than failing. It keeps one record of the installed set and nothing beside it: what is installed, what each plugin declares about itself, and what vouched for it live together, so nothing has to be kept in step. A package is either fetched through a package source, which says what vouches for its bytes, or already on the machine, which nothing vouches for; whether vouching is required is the host's one policy. Nor does the host run anything on a plugin's behalf: there are no declared schedules, and a plugin's background work is its own, registered the way everything else it does is, which is what lets disabling the plugin stop it along with everything else it registered. * Provides: install, remove, enable, disable, kill; the package source seam, and whether vouching is required over it; each plugin's folder. * Not responsible for: what flows decide to do, or their background work. ### Trace Traceability first, with observability derived from it rather than collected beside it. Observability asks whether an operator can infer what a system is doing, and normally pays for the answer with sampling, aggregation, and continuous export; traceability asks what this node did, on whose behalf, and whether the record of it can be shown to somebody else and believed. Traceability is the stricter of the two -- nothing sampled, attribution derived rather than supplied, the record tamper-evident -- which is what makes a trace a sound source to read metrics out of instead of standing up a second pipeline. The containment stops at what this plugin is willing to hold: it is deliberately small, low-volume, and synchronously written, and keeps only a bounded tail, so anything high-frequency or long-horizon is materialized outside it. Every message sent, model run executed, revision authored, authorization granted, and flow step taken leaves a record, attributed to the identities involved, so you can always reconstruct what your representative did and why. Each record carries the hash of the one before it and the head is signed with the node key, so an omission breaks the chain and an excerpt shared with a peer verifies against a signed checkpoint. Traces are local-first: sharing any part of one is a policy decision, and attribution is strong enough that reputation can later be built on it, per identity rather than per node. * Provides: append, from every core plugin; reconstruct a session, a day, a peer's history; a signed checkpoint over the chain. * Not responsible for: evaluating what it records. ## Flows A flow declares up front what it is and what it will need -- the data it wants to learn about, the actions it wants gated -- so the principal can make an informed install decision. Once installed, a flow reaches the rest of the node only through the core plugins' caller-aware operations: through them it messages peers, finds them, invokes the intelligence loop, keeps its own state, negotiates over a shared versioned artifact, learns what policy allows about the principal's sources, asks the principal for authorization, and leaves traces. Those operations never hand over raw contents, credentials, or keys: a flow names what to use and where it should go, and the operation is performed on its behalf. There is no core flow beneath the others. The fixed layer is the envelope: every node speaks it, and the plugin id and version it carries are what route a message to the flow and session that own it, so a flow is an application on that layer rather than a continuation of some base conversation. What flows share is factored downward instead, into flow services -- called rather than spoken -- so common machinery costs nothing on the wire and no session ever carries two names. ## Worked example: a joint query Two principals ask "which songs would we both like?" without either seeing the other's listening data. The flow, with the core plugin doing the work in parentheses: 1. Each principal ingests listening data as a named source (data). 2. Node A publishes the query so peers can find it (discovery). 3. Node B sees it and triages: one model run over what B holds decides whether this principal can contribute (intelligence). 4. A probes B; a session opens (network, identity). B answers from its stored verdict, with no new model run. 5. Both are willing, so the initiator is chosen by the byte order of the two identities on the session: both sides compute the same answer from ids they already hold, and exactly one initiation exists. 6. The nodes negotiate objectives: each side's agent proposes and reviews revisions of a shared artifact until one side marks the other's revision agreed and both then hold that same agreed revision, within a bounded number of rounds (intelligence, an artifact store, network). Neither agent sees the other node's data. 7. For each agreed objective, the same machinery negotiates a program the same way: propose, review, agree. 8. The flow verifies a joint-computation box both sides can check and neither owns, confirms its own copy of the agreed program by hash, stakes exactly the sources the objective names into it through an operation -- so the flow never holds them -- and fetches its own output (data, under authorization). 9. One more model run summarizes the result for the principal (intelligence), who has been able to watch every step (api, trace). Note what the node did not contain: the joint-computation box, the negotiation strategy, the triage question. Those belong to the flow. The node supplied messages, model runs, authorization, and traces. This is what a written-down flow looks like; the [meta flow](/architecture#the-flow) negotiates one like it from a principal's intent instead of shipping it. *** Built today: [architecture](/architecture). What to clone: [repositories](/repos). # Architecture What a node is made of on `main`, as of the date in the footer. The [design](/design) page says what is meant; this page says what exists, where, and what does not yet. Every claim names a file, a key, or a route in the repository that owns it, and [repositories](/repos) says which of those to clone. ## Two processes A node is two processes. **`pac-daemon`** holds the identity, the wire, the stores, and the installed plugins: it boots the spine, serves the control API, and supervises the worker -- restarting it when it dies, stopping it first on shutdown ([`daemon/src/main.ts`](https://github.com/project-pac/pac-node/blob/main/daemon/src/main.ts)). **`pac-worker`** executes model runs in a process of its own and never opens the node's store: it is told `PAC_DATA_DIR` and `PAC_API_URL`, pulls turns over three routes of the control API -- `POST /worker/claim`, `POST /worker/turns/:runId/:turn/renew`, `POST /worker/turns/:runId/:turn/settle` ([`sdk/src/worker.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/worker.ts)) -- and holds each under a lease it renews while a model runs. The model's process is the least trusted thing in the node, which is why the split is a process boundary and not a module one. ``` principal ── pac cli · mcp · page · desktop ──▶ 127.0.0.1:7452 (a token when not loopback) │ ┌──────────────────────────── pac-daemon ─────────────┴──────────────────────────────┐ │ spine.yml trace identity network discovery intelligence data host api │ │ plugins.yml transport-iroh discovery-directory records … operation-jc-box meta │ │ core routes /health /metrics /traces /sessions /peers /runs /plugins │ │ plugin routes /flows//* worker routes /worker/* │ └──────┬──────────────────────────┬──────────────────────────────┬───────────────────┘ │ iroh (QUIC) │ https, writes signed │ claim · renew · settle ▼ ▼ ▼ a peer's pac-daemon node-directory.projectpac.net pac-worker plugin-registry.projectpac.net defaultWorker/executors.yml executor-cli rows: pi | claude operation-jc-box ── RA-TLS, pinned measurements ──▶ the box (jc-tee-vm) ``` ## The data directory One folder per plugin, core and non-core alike, directly under the data directory (`~/.pac/data` for a node the cli set up). A plugin's folder is its own: its database, its keys, whatever it wrote. Removing the plugin leaves the folder for a later install of the same id to find. From [`pac-node/README.md`](https://github.com/project-pac/pac-node/blob/main/README.md): ``` / spine.yml plugins.yml the entry files modules/ an npm prefix: what setup seeded, what a source fetched trace/ identity/ network/ intelligence/ defaultWorker/ the worker's own: its executors.yml, and a folder per executor row / one per installed plugin ``` The names the node's own state uses are refused as plugin ids at install ([`core/host/src/layer.ts`](https://github.com/project-pac/pac-node/blob/main/core/host/src/layer.ts), `RESERVED_DIR_NAMES`). A plugin's folder is derived from its identity, never passed; `readUnderCaller` is the one bounded way another plugin's files are opened for it, and it refuses a path that escapes ([`sdk/src/files.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/files.ts)). ## The spine Eight rows in `spine.yml`, one per core plugin, written on first boot with every default filled in and thereafter only added to ([`daemon/src/spine.ts`](https://github.com/project-pac/pac-node/blob/main/daemon/src/spine.ts), `spineRows`). In boot order: | row | package | claims | state | | -------------- | ------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------------------- | | `trace` | `@projectpac/core-trace` | `ctx.trace` | built | | `identity` | `@projectpac/core-identity` | `ctx.identity` | built | | `network` | `@projectpac/core-network` | `ctx.network` | built | | `discovery` | `@projectpac/core-discovery` | `ctx.discovery` | built | | `intelligence` | `@projectpac/core-intelligence` | `ctx.intelligence` | built | | `data` | `@projectpac/core-data` | nothing | **a stub**: mounts, claims no service key, does nothing ([README](https://github.com/project-pac/pac-node/blob/main/core/data/README.md)) | | `host` | `@projectpac/core-host` | `ctx.host`, and provides `ctx.self` | built | | `api` | `@projectpac/core-api` | `ctx.api` | built | Trace boots first because it must record what a node did whether or not the node has a key yet. A failed spine row aborts the boot; a failed installed plugin appears in the settle audit with its reason and the node keeps running ([`daemon/README.md`](https://github.com/project-pac/pac-node/blob/main/daemon/README.md)). Installed plugins are the rows of `plugins.yml`, the host's to rewrite, and which tree an entry belongs to is what tells a core plugin from an installed one when a caller is derived. ## What a plugin compiles against `@projectpac/sdk` ([`pac-node/sdk`](https://github.com/project-pac/pac-node/tree/main/sdk)): contracts and the schemas that parse them at a boundary, no behaviour. Importing anything from it brings the one cordis `Context`/`Events` declaration merge into scope ([`sdk/src/context.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/context.ts)), which is why a plugin never imports a core package. | `ctx.` | contract | what it is | | -------------- | --------------- | ----------------------------------------------------------------------------------------- | | `identity` | `Identity` | node id, identities, sign, verify | | `network` | `Network` | `route`, `openSession`, `send`, `fetch`, `serve`, `closeSession` | | `discovery` | `Discovery` | `advertise` (returns a disposer), `query`, `adapter` | | `intelligence` | `Intelligence` | `startRun`, `appendTurn`, `startAndAppendTurn`, `closeRun`, `get` | | `host` | `Host` | `install`, `remove`, `enable`, `disable`, `kill`, `list`, `packageSource` | | `trace` | `Trace` | `append`, `query`, `checkpoint`, reconstruct a session or a run | | `api` | `ControlApi` | `handle`, `callRoute` | | `self` | `PluginBinding` | the calling plugin's `id`, its default network identity `node`, its `dir`, `log` | | `executors` | `Executors` | the worker's registry -- provided in the worker process alone | | event | dispatched by | when | | ---------------------- | ------------- | ------------------------------------------------------------------------------------ | | `core/message` | network | an envelope for one of this plugin's sessions arrived | | `core/message-failed` | network | an outbound message exhausted its retries | | `core/turn-settled` | intelligence | one settled turn, into the scope of the plugin that started the run, once | | `core/removing` | host | this plugin is being removed and is still itself -- the only exactly-once event | | `core/peer-unreachable` | network | between delivery retries; discovery listens and re-runs its remembered needs | Events are dispatched in parallel mode and delivered at least once, so a handler is idempotent on the ids it carries. A **manifest** ([`sdk/src/manifest.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/manifest.ts)) is `id`, `kind`, `serviceKey?`, `version`, `payloadTypes`, `routes`. `kind` is one of `flow`, `flow-service`, `source`, `operation`, `tool`, `executor`, `transport`, `discovery-adapter`, `package-source`; it is descriptive, and nothing in the kernel loads, routes, or resolves by it. The manifest is declared in TypeScript and copied into `package.json#pac.manifest` by [`flow-devkit/src/sync-manifests.ts`](https://github.com/project-pac/pac-plugins/blob/main/flow-devkit/src/sync-manifests.ts), gated by `manifests:check`, so a node reads it without importing the module. `SPOKEN_KIND = "flow"` ([`sdk/src/discovery.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/discovery.ts)) is the advertisement kind every flow announces itself under; it shares a string with the manifest kind and nothing couples them. Two surfaces, not one. `Context` is what an installed plugin compiles against. `InternalSurfaces` ([`sdk/src/readers.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/readers.ts)) is what one core plugin, the daemon, or the control API reads of another core plugin -- session readers, run readers, the turn broker -- reached through `internals(ctx, key)` and never widened at a call site. The layering is a test, not a convention: the sdk depends on cordis and zod alone, `lib` on the sdk at most, core plugins never on a process, and no `link:` in any manifest ([`sdk/tests/dependencyRule.spec.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/tests/dependencyRule.spec.ts)). ## Caller derivation Every seam repeats one mechanism. An operation derives which plugin called it from the accessing context ([`sdk/src/caller.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/caller.ts), `callerOf`): the fiber is walked to the loader entry, and the tree that entry belongs to -- `spine` or `plugins` -- says whether the caller is core or installed. Nothing is named by the caller, so nothing can be claimed for another plugin. Every registration a plugin makes -- a route, an advertisement, a served resource, a record store -- is an effect of the calling scope, so disabling the plugin withdraws all of it with no disposer anybody has to remember to call. `ctx.self` is one service answering per caller ([`core/host/src/binding.ts`](https://github.com/project-pac/pac-node/blob/main/core/host/src/binding.ts)). This is why the host does not sit between a plugin and the core: there is nothing for it to mediate that the seams do not already know. ## The wire An envelope ([`sdk/src/network.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/network.ts)) is `sender`, `senderPublicKeyPem`, `recipient`, `plugin`, `pluginVersion`, `sessionId`, `messageId`, `payloadType`, `payload`, `signature`; the signature covers every field above it, and the carried key derives `sender`, so an envelope self-certifies. Bounds ([`core/network/src/wire.ts`](https://github.com/project-pac/pac-node/blob/main/core/network/src/wire.ts)): `MAX_FRAME_BYTES` 1 MiB, `MAX_PAYLOAD_BYTES` 256 KiB, `MAX_RESOURCE_BYTES` 8 MiB. Inbound, in order: the key derives the claimed sender, the signature verifies, the envelope names this node, and it names a plugin and version this node has a route for; then dedupe, open the session if it is new, store, ack, dispatch `core/message`. A refusal tells the sender to stop retrying. Outbound is an outbox: a message is signed when queued so a retry is identical bytes, claimed under a lease, retried `maxAttempts` (5) times from `retryDelayMs` (1 s), and reported as `core/message-failed` when exhausted ([`core/network/src/index.ts`](https://github.com/project-pac/pac-node/blob/main/core/network/src/index.ts)). A session belongs to one plugin: `send` against another plugin's session is refused, and so is a payload type the plugin's `route` did not declare. `fetch` rides the envelope under the reserved type `network/fetch`. The one transport is [`@projectpac/transport-iroh`](https://github.com/project-pac/pac-plugins/blob/main/adapters/network/transports/iroh/README.md): QUIC, one ALPN, its own key, relays as config; it moves bytes and reads nothing. ## The control API Fastify, bound to `127.0.0.1:7452` by default ([`core/api/src/index.ts`](https://github.com/project-pac/pac-node/blob/main/core/api/src/index.ts)); a non-loopback bind without a `token` of at least 16 characters is refused at load and again in the daemon's preflight. Core routes: | method | path | | -------- | --------------------------------------------------------------------------------------------- | | `GET` | `/health`, `/metrics`, `/traces`, `/sessions`, `/peers`, `/runs`, `/plugins` | | `POST` | `/plugins` (install), `/plugins/:id/enable`, `/plugins/:id/disable`, `/plugins/:id/kill` | | `DELETE` | `/plugins/:id` | | `POST` | `/worker/claim`, `/worker/turns/:runId/:turn/renew`, `/worker/turns/:runId/:turn/settle` | | any | `/flows/:plugin/*` -- every plugin's own routes | A plugin registers a route with `ctx.api.handle(method, path, handler)`. The caller is derived, the pair is refused unless the plugin's manifest declared it, and it is served at `/flows//` -- one catch-all rather than a Fastify route per registration, because plugins come and go after the server is listening. `callRoute(plugin, method, path, body)` is the same dispatch without HTTP, run as the target plugin's own handler; the router uses it so the work it starts is attributed to the flow whose route it is. ## Runs, turns, the worker A run ([`sdk/src/intelligence.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/intelligence.ts)) names an executor row and carries a payload for it and metadata for the caller, both stored and never read by the kernel. Runs are linear: the claim rule ([`core/intelligence/src/store.ts`](https://github.com/project-pac/pac-node/blob/main/core/intelligence/src/store.ts)) hands out a pending turn only when no earlier turn of its run is still open. The worker holds a claimed turn under a lease of `turnLeaseMs` (30 s), renewing at a third of it; a lapsed lease fails the turn as `interrupted` and never re-queues it, because a model may already have acted. `core/turn-settled` is dispatched once into the scope of the plugin that started the run, and dispatch is gated until the daemon says the plugins tree has settled, so a turn that settled while the node was down is not announced into an empty scope. A worker silent for `workerStaleMs` (30 s) is counted dead. The worker composes its own executors from `defaultWorker/executors.yml`. The one executor is [`@projectpac/executor-cli`](https://github.com/project-pac/pac-plugins/blob/main/adapters/intelligence/executors/cli/README.md): a command line that takes the prompt on stdin and answers on stdout. `pi` and `claude` are two rows of it, not two packages, and a turn's process inherits nothing of the parent's environment but what the row passes through. It has no manifest because the worker mounts it, not the host. Prompt composition and output parsing are not the kernel's: [`@projectpac/prompting`](https://github.com/project-pac/pac-plugins/blob/main/flow-services/prompting/README.md) is functions a flow calls -- `composePrompt`, `interpret` -- with the context delimited and labelled as data, so text of unknown origin cannot close a delimiter. ## Identity, discovery, trace **Identity** ([`core/identity`](https://github.com/project-pac/pac-node/blob/main/core/identity/README.md)): ed25519. The node id is the sha256 of the public key's DER, as hex. `sign(bytes, as?)` unnamed signs with the node key; `identities()` lists the network identities and excludes it. Keys are written aside, mode 0600. **Discovery** ([`core/discovery`](https://github.com/project-pac/pac-node/blob/main/core/discovery/README.md)): `advertise` returns a disposer that withdraws from every adapter that took the advertisement; `query` returns candidates as `{peer, advertisement, adapter}`; the plugin re-registers this node with every adapter every `republishMs` (30 s). An adapter that has not answered in `queryTimeoutMs` (5 s) is treated as down, and the last mirrored answer is returned and traced. The one adapter is [`@projectpac/discovery-directory`](https://github.com/project-pac/pac-network/blob/main/services/node-directory/plugin/README.md), which lives in pac-network beside the server it speaks to, because the two halves share `protocol.ts`. **Trace** ([`core/trace`](https://github.com/project-pac/pac-node/blob/main/core/trace/README.md)): `trace/trace.jsonl`, appended synchronously; `append` never fails into the caller's path. Each record's `prev` is the sha256 of the canonical JSON of the record before it, from a genesis of 64 zeros ([`chain.ts`](https://github.com/project-pac/pac-node/blob/main/core/trace/src/chain.ts)). A checkpoint -- `at`, `head`, `count`, signed by the node key under `pac/trace-checkpoint/1` -- is written every `checkpointEveryMs` (5 min). Records older than `retainMs` (30 days) move to an archive beside the file every `pruneEveryMs` (1 h); nothing is deleted. The actor is derived from the caller, the trigger is supplied. ## The host Install has two shapes ([`core/host/src/index.ts`](https://github.com/project-pac/pac-node/blob/main/core/host/src/index.ts)). `{ package }` names something the node does not have: an enabled package source fetches it into `modules/`, the host reads the manifest out of the package's own `package.json` -- executing nothing -- and the row carries what the source said vouched for the bytes. `{ source, id }` names a module already on the machine; its provenance is `missing`, by `hand`. `Provenance` is `{ vouched: "verified" | "invalid" | "missing", by, evidence? }` ([`sdk/src/host.ts`](https://github.com/project-pac/pac-node/blob/main/sdk/src/host.ts)), and the kernel reads `vouched` alone. `Config.accept` maps a source id to `"verified"` or `"any"`, with `*` for the rest and `any` the default; `invalid` is accepted by nobody. The layer rule ([`layer.ts`](https://github.com/project-pac/pac-node/blob/main/core/host/src/layer.ts)) refuses a fetched package that claims a spine service key or a reserved name. `remove` dispatches `core/removing` into the plugin while it still runs and leaves its folder; `kill` differs from `disable` only across a restart. Three package sources exist: [`packages-npm`](https://github.com/project-pac/pac-plugins/blob/main/adapters/host/package-sources/npm/README.md) (`npm install --ignore-scripts`, names or paths only), [`packages-git`](https://github.com/project-pac/pac-plugins/blob/main/adapters/host/package-sources/git/README.md) (`git+https://…#ref`, shallow), and [`packages-checkouts`](https://github.com/project-pac/pac-plugins/blob/main/adapters/host/package-sources/checkouts/README.md) (a registry name answered from a working tree beside this one -- what every dev lane installs through, so a lane runs the same install a released node runs). ## Installed plugins Everything below lives in [pac-plugins](https://github.com/project-pac/pac-plugins) at `adapters///` or `flow-services/`, except where a row says otherwise, and the table mirrors that tree. Each row links its README. | kind | package | claims | what | | ----------------- | ------------------------------------------------------------------------------------------- | ----------------------- | ------------------------------------------------------------------------------------ | | operation | [`operation-jc-box`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/operations/jc-box/README.md) | `ctx.jcBox` | submit named sources to a verified box, encrypted; the party key is the adapter's | | operation | [`operation-llm`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/operations/llm/README.md) | `ctx.llm` | the python a box program needs to ask a model; makes no call itself | | operation | [`operation-tsc`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/operations/tsc/README.md) | `ctx.tsc` | type-check TypeScript a flow was handed | | operation | [`operation-pexe`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/operations/pexe/README.md) | `ctx.pexe` | compile an object-toolchain draft and build its box envelope | | operation | [`operation-dobj`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/operations/dobj/README.md) | `ctx.dobj` | the write half of an object daemon | | source | [`source-web-exports`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/sources/web-exports/README.md) | `ctx.webExports` | data a service handed the principal back, with a sentence saying what is inside | | source | [`source-skills`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/sources/skills/README.md) | `ctx.skills` | procedures the principal wrote; the front matter kept apart from the body | | source | [`source-llm`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/sources/llm/README.md) | `ctx.models` | the principal's model access, offered like anything else they hold | | source | [`source-agent-history`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/sources/agent-history/README.md) | `ctx.agentHistory` | what they work on, read from their coding-agent transcripts by a model | | source | [`source-conversation-notes`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/sources/conversation-notes/README.md) | `ctx.conversationNotes` | their conversations, by way of files the assistant writes about itself | | source | [`source-intents`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/sources/intents/README.md) | `ctx.intents` | what they want done, extracted from the other sources | | package-source | [`packages-npm`](https://github.com/project-pac/pac-plugins/blob/main/adapters/host/package-sources/npm/README.md), [`packages-git`](https://github.com/project-pac/pac-plugins/blob/main/adapters/host/package-sources/git/README.md), [`packages-checkouts`](https://github.com/project-pac/pac-plugins/blob/main/adapters/host/package-sources/checkouts/README.md) | -- | registered on `ctx.host` | | executor | [`executor-cli`](https://github.com/project-pac/pac-plugins/blob/main/adapters/intelligence/executors/cli/README.md) | -- | mounted by the worker's `executors.yml` | | transport | [`transport-iroh`](https://github.com/project-pac/pac-plugins/blob/main/adapters/network/transports/iroh/README.md) | -- | registered on `ctx.network` | | flow-service | [`artifacts-git`](https://github.com/project-pac/pac-plugins/blob/main/flow-services/artifacts-git/README.md) | `ctx.artifacts` | the versioned resource two peers negotiate over; git for history and diff | | flow-service | [`negotiation`](https://github.com/project-pac/pac-plugins/blob/main/flow-services/negotiation/README.md) | `ctx.negotiation` | the suggest-and-agree rounds, as a factory a flow drives with its own hands | | flow-service | [`jc-box-run`](https://github.com/project-pac/pac-plugins/blob/main/flow-services/jc-box-run/README.md) | `ctx.jcBoxRun` | the box session as durable steps: party, propose, view, approve, submit, fetch | | flow-service | [`records`](https://github.com/project-pac/pac-plugins/blob/main/flow-services/records/README.md) | `ctx.records` | a plugin's durable rows, in its own folder, the caller derived | | flow-service | [`results`](https://github.com/project-pac/pac-plugins/blob/main/flow-services/results/README.md) | `ctx.results` | what every flow produced, read in one place | | flow-service | [`sources`](https://github.com/project-pac/pac-plugins/blob/main/flow-services/sources/README.md) | `ctx.sources` | what this node holds, read in one place | | -- | [`prompting`](https://github.com/project-pac/pac-plugins/blob/main/flow-services/prompting/README.md) | -- | functions, not a plugin: compose a prompt, interpret the answer | | flow-service | [`router`](https://github.com/project-pac/pac-router/blob/main/README.md), in its own repository | `ctx.router` | a sentence decided into a declared intent and started through `callRoute` | | discovery-adapter | [`discovery-directory`](https://github.com/project-pac/pac-network/blob/main/services/node-directory/plugin/README.md), in pac-network | -- | registered on `ctx.discovery` | Also in pac-plugins and not plugins: [`lib/`](https://github.com/project-pac/pac-plugins/blob/main/lib/README.md), `@projectpac/plugin-lib` -- the artifact-store contract, the material a source hands over, the offers a descriptor carries, and the helpers every flow writes first -- and [`flow-devkit/`](https://github.com/project-pac/pac-plugins/blob/main/flow-devkit/README.md), the lane scripts and the fixture corpus the flow repositories share. `artifacts-memory` is the artifact store's test double and is not published. A source answers `inventory()` -- the offers it puts in a descriptor for a peer -- and `stake()`; a flow never reads a source's contents. ## Services that are nobody's node Both live in [pac-network](https://github.com/project-pac/pac-network), both belong behind https, and anyone may run either: a network is whichever nodes point at the same ones. | | node directory | plugin registry | | ------------ | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | package | [`@projectpac/node-directory`](https://github.com/project-pac/pac-network/blob/main/services/node-directory/server/README.md) | [`@projectpac/plugin-registry`](https://github.com/project-pac/pac-network/blob/main/services/plugin-registry/server/README.md) | | holds | claims nodes make about themselves: presence, and advertisements | one operator's list of what a node could install, with a sentence per entry | | who writes | any node, signed by its node key under a domain string, within `MAX_SIGNED_AGE_MS` (5 min) | the operator, with a bearer token; the registry does not start without one | | routes | `PUT /nodes/:node`, `PUT /advertisements`, `POST /withdrawals`, `POST /queries`, `GET /health` | `GET /plugins`, `GET /plugins/:id/readme`, `PUT /plugins/:id`, `DELETE /plugins/:id`, `GET /health` | | lifetime | presence is a read-time filter: a node not heard from in `presenceTtlMs` (5 min) stops being returned; nothing is deleted | durable until removed; the catalogue is registered by walking checkouts' manifests, never committed | | default port | 7460 (the lanes run it on 7451) | 7461 | | production | `node-directory.projectpac.net` | `plugin-registry.projectpac.net` | The protocol is owned by the plugin half ([`protocol.ts`](https://github.com/project-pac/pac-network/blob/main/services/node-directory/plugin/src/protocol.ts)) and imported by the server, because they are two halves of one wire. ## The box [jc-tee-vm](https://github.com/project-pac/jc-tee-vm) is the joint-computation box: a long-lived attested runtime that runs a program every party approved, once, over inputs only it decrypts. It terminates TLS 1.3 itself with a boot-time key and carries its attestation quote inside the certificate (`report_data = sha256(cert SPKI)`), so a client verifies the runtime during the handshake and every request rides the attested channel ([`src/common/ra-tls.ts`](https://github.com/project-pac/jc-tee-vm/blob/main/src/common/ra-tls.ts)). The protocol: 1. `POST /session` with the program, the runner, and the participants' keys. The set and the runner are fixed here; the reply is the session id and `sha256(program)`. 2. Each party reviews `GET /session/:id/program` offline. 3. `POST /session/:id/approve`: a signature over session, runner, program hash, and the declared egress. Unanimous approval opens inputs. 4. `POST /session/:id/input`, encrypted to the enclave. The last input starts the run, asynchronously. 5. Poll `GET /session/:id` until `COMPLETE` or `FAILED`. 6. `POST /session/:id/output`, signed: each party receives its own output, encrypted to its own key. Raw inputs were wiped after the run. Runners are `wasm`, `python` -- source text, run by an interpreter baked into the measured image; the recommended path, because the reviewed artifact is the executed artifact -- and `dobj`. `TEE_MODE` is `mock`, `dstack`, `gcp`, `aws`, or `nitro`, and the client's policy is the same in every mode. From a node, [`operation-jc-box`](https://github.com/project-pac/pac-plugins/blob/main/adapters/data/operations/jc-box/README.md) loads the `jc-tee-vm` client by name at first use and drives the session; its `egress` allowlist -- the hosts the program may reach -- is bound into every approval signature. The production box, its mode, and its measurement pins are what a node is set up with, in [`onboard/src/defaults.json`](https://github.com/project-pac/pac-ux/blob/main/onboard/src/defaults.json); they change when the box is redeployed, so they are not repeated here. ## What a joined node runs `pac join` installs the plugin set in [`pac-ux/onboard/src/defaults.json`](https://github.com/project-pac/pac-ux/blob/main/onboard/src/defaults.json), keyed by when each applies: | when | plugins | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | joined | `transport-iroh`, `discovery-directory` | | meta | `records`, `results`, `sources`, `negotiation`, `jc-box-run`, `artifacts-git`, `packages-npm`, `operation-tsc`, `operation-llm`, `operation-jc-box`, and `@projectpac/flow-meta` with `sources: [skills, webExports, models, conversationNotes, agentHistory]` | | captured | `source-agent-history`, `source-conversation-notes` | | derived | `source-intents` | The executor is `@projectpac/executor-cli`, the package source `@projectpac/packages-npm`, and the models `pi` or `claude`. The router is not in the set. ## The flow [`@projectpac/flow-meta`](https://github.com/project-pac/pac-flow-meta/blob/main/README.md) is the one flow a joined node runs. A principal states an intent; the two nodes' agents, each seeing only the other side's descriptor, negotiate a **design** -- what the flow will do, which of each node's sources it may stake, what messages it exchanges, whether it computes in the box and what may come out -- and then the **package** that implements it: a small TypeScript plugin with no runtime dependencies, which both nodes install under one id and the initiator starts. The negotiation is the engine's rounds over an artifact store: one side suggests, the other reads the change and suggests back, until one agrees. A draft is gated before it travels ([`src/gate.ts`](https://github.com/project-pac/pac-flow-meta/blob/main/src/gate.ts)): the exact file set, an id derived from the task, the four named exports and no default, imports of node's builtins alone, syntax node's type stripper accepts, and -- where the node has `operation-tsc` -- a type check against [`src/library/pac.d.ts`](https://github.com/project-pac/pac-flow-meta/blob/main/src/library/pac.d.ts), the surface of a node as one file. What that file declares is exactly what a generated flow may rely on: `self`, `identity`, `network`, `discovery`, `intelligence`, `api`, `trace`, `artifacts`, `negotiation`, `jcBox`, `jcBoxRun`, and the five events. It does not declare `records`, `results`, or `router`. Both nodes run the gate again over the agreed bytes before installing, and exchange a hash of what they installed. Either half may `decline`, and the task ends `declined` with the reason: a first-class outcome, not a failure. Generated plugins are `meta-`, package `@projectpac-generated/meta-`, with `POST /start` and `GET /status`; they are installed by path, so a host that accepts only verified packages cannot run this flow. [`@projectpac/flow-template`](https://github.com/project-pac/pac-flow-template/blob/main/src/index.ts) is the copy-me: one file that does the smallest complete network thing and touches every seam a flow touches -- `ctx.network.route` first, an advertisement under `SPOKEN_KIND` retried until it sticks, a `core/message` handler, its own timers as effects of its scope, `ctx.inject(["router"])` and `ctx.inject(["results"])` from optional scopes so a node without either still runs it, `ctx.api.handle` for its routes, and one model run through `startAndAppendTurn` with `composePrompt` and `interpret`. [skills](https://github.com/project-pac/pac-flow-skills), [joint-search](https://github.com/project-pac/pac-flow-joint-search) and [objects](https://github.com/project-pac/pac-flow-objects) are the hand-written flows meta now negotiates; objects is parked, because it needs an object daemon nothing here ships. ## Not built yet * **The data core plugin.** `core/data` mounts, claims no service key, and does nothing ([README](https://github.com/project-pac/pac-node/blob/main/core/data/README.md)). Sources and operations are plugins in pac-plugins, each claiming its own key. * **Policy and authorization.** No policy service exists; `POLICY_DENIED` is an error code with nothing that raises it. The one refusal made on a principal's behalf is `operation-jc-box`'s `egress` allowlist. * **One `apply` verb.** Each operation adapter is its own verb behind its own key (`ctx.jcBox`, `ctx.llm`, `ctx.tsc`, `ctx.pexe`, `ctx.dobj`), deriving its caller; `readUnderCaller` is the bounded read of another plugin's files. * **Sources into a generated flow.** flow-meta copies the sources the design assigned to this node into `//sources/` before installing it and removes them at teardown ([`src/install.ts`](https://github.com/project-pac/pac-flow-meta/blob/main/src/install.ts)): the one documented place a plugin writes into another's folder, and the seam a data operation would replace. * **Folder permissioning**, and what a run's sandbox may see of the data directory. * **The router in the joined set.** It exists and is installable; flow-meta declares no intents to it. * **Registry submissions**, per-publisher credentials, and review: every entry is the operator's, published by walking checkouts. * **Directory accounts and rate limits.** * **Sharing a trace with a peer.** Checkpoints are signed; there is no export path. *** Checked against `main` on 2026-09-12: pac-node, pac-plugins, pac-network, pac-router, pac-ux, pac-flow-meta, pac-flow-template, jc-tee-vm. What is meant: [design](/design). What to clone: [repositories](/repos). # Repositories PAC is a set of sibling git repositories under one parent directory, and they find each other by relative path. pac-node is the root: every other checkout resolves `cordis` through it (`overrides` in each `pnpm-workspace.yaml`), so pac-node installs first and the rest follow the order written in [`install-constellation.sh`](https://github.com/project-pac/pac-ci/blob/main/constellation/install-constellation.sh). Nobody needs all of them; the first table says which subset each job wants. This page is a map, and each repository's README is the reference, so it is linked rather than restated. ## What do you want to do | do | check out, in install order | read first | run | | --- | --- | --- | --- | | run a node of your own | nothing -- install the cli | [INSTALL.md](https://github.com/project-pac/pac-ux/blob/main/INSTALL.md) | the install one-liner, which hands over to `pac onboard` | | run one node from source | pac-node | [pac-node/README.md](https://github.com/project-pac/pac-node/blob/main/README.md) | `just demo` | | understand the design | this site | [Design](/design), then [sdk/README.md](https://github.com/project-pac/pac-node/blob/main/sdk/README.md) | -- | | watch two nodes write a flow | pac-node pac-plugins pac-network pac-flow-meta pac-ux, and jc-tee-vm beside them | [pac-flow-meta/README.md](https://github.com/project-pac/pac-flow-meta/blob/main/README.md), [devtools/README.md](https://github.com/project-pac/pac-flow-meta/blob/main/devtools/README.md) | `MODEL=claude MODEL_ARG="-p --model sonnet" just dev`, then `just intent "..."` | | write a flow | pac-node pac-plugins pac-network pac-router pac-ux, and a copy of pac-flow-template | [pac-flow-template/README.md](https://github.com/project-pac/pac-flow-template/blob/main/README.md), then [src/index.ts](https://github.com/project-pac/pac-flow-template/blob/main/src/index.ts) | `just dev` in your flow, `just a1 api GET /flows//board` | | write an adapter -- a source, an operation, a transport, a package source, an executor | pac-node pac-plugins; a flow lane's set to try it live | [pac-plugins/README.md](https://github.com/project-pac/pac-plugins/blob/main/README.md), then the sibling adapter of the same kind | `pnpm check`; then a lane's `just dev` and `just a1 plugins add --package ` | | change the kernel | pac-node | [pac-node/README.md](https://github.com/project-pac/pac-node/blob/main/README.md), [CLAUDE.md](https://github.com/project-pac/pac-node/blob/main/CLAUDE.md) | `pnpm check`, `just demo` | | change the cli, the page, the mcp server, or the app | pac-node pac-plugins pac-network pac-ux | [pac-ux/README.md](https://github.com/project-pac/pac-ux/blob/main/README.md) | `just dev`, `just dev-two` | | host a directory or a registry | pac-node pac-network | [pac-network/README.md](https://github.com/project-pac/pac-network/blob/main/README.md) | `just up` | | run or change the box | jc-tee-vm alone -- npm, outside the constellation | [jc-tee-vm/README.md](https://github.com/project-pac/jc-tee-vm/blob/main/README.md) | `TEE_MODE=mock PORT=3000 npm run server` | | fix ci | pac-ci | [pac-ci/README.md](https://github.com/project-pac/pac-ci/blob/main/README.md) | `smoke.yml` by hand, then move `constellation-v1` | pac-ux is in the flow rows because every flow lane runs the cli from source (`node ../pac-ux/cli/src/main.ts`). A flow's own ci does not need it, because its suites do not. ## The repositories ### The node | repo | what | needs beside it | | --- | --- | --- | | [pac-node](https://github.com/project-pac/pac-node) | the kernel: `sdk/` (what a plugin compiles against), `lib/`, the eight core plugins under `core/`, `daemon/`, `worker/`, `testing/`. `core/data` is a stub that mounts and does nothing; [its README](https://github.com/project-pac/pac-node/blob/main/core/data/README.md) says what it is for | nothing of PAC's | | [pac-plugins](https://github.com/project-pac/pac-plugins) | every installable plugin that is not a flow, at `adapters///`; the flow services under `flow-services/`; `lib/` (`@projectpac/plugin-lib`, the contracts adapters implement); `flow-devkit/` (the lane scripts and fixtures the flow repositories share). No justfile: `pnpm check` only | pac-node | | [pac-router](https://github.com/project-pac/pac-router) | one flow service, `ctx.router`: a principal's sentence decided into an intent some flow declared, and started through that flow's own route | pac-node pac-plugins pac-network | ### The network | repo | what | needs beside it | | --- | --- | --- | | [pac-network](https://github.com/project-pac/pac-network) | what the project hosts so a node does not have to: the node directory (`services/node-directory/`, the server and the plugin a node installs to reach it) and the plugin registry (`services/plugin-registry/`) | pac-node | ### The ux | repo | what | needs beside it | | --- | --- | --- | | [pac-ux](https://github.com/project-pac/pac-ux) | the clients of the control api: `lib` (`@projectpac/client`), `onboard`, `cli` (`pac`), `mcp`, `web`, `desktop`; `install.sh` and the `INSTALL.md` generated beside it | pac-node pac-plugins for the gate; all nine for a desktop release, which packs them | ### The flows | repo | what | needs beside it | | --- | --- | --- | | [pac-flow-meta](https://github.com/project-pac/pac-flow-meta) | the flow a joined node runs: state an intent, and the two nodes' agents negotiate a design, then the package that implements it, which both install and run. `src/library/pac.d.ts` is the surface of a node as one file | pac-node pac-plugins pac-network | | [pac-flow-template](https://github.com/project-pac/pac-flow-template) | the smallest complete flow, one file. Copy the repository to start your own; there is no generator | pac-node pac-plugins pac-network pac-router | | [pac-flow-skills](https://github.com/project-pac/pac-flow-skills), [pac-flow-joint-search](https://github.com/project-pac/pac-flow-joint-search), [pac-flow-objects](https://github.com/project-pac/pac-flow-objects) | prior art: a method one node holds run for another; a question neither side's data answers alone; an agreed action over objects each side owns (parked -- it needs an object daemon nothing here ships). Hand-written flows of the kind meta now negotiates; read them for how a negotiation is scripted, not as templates | as the template | ### The box | repo | what | needs beside it | | --- | --- | --- | | [jc-tee-vm](https://github.com/project-pac/jc-tee-vm) | the joint-computation box: an attested runtime that runs a program every party approved, once, over inputs only it decrypts, and hands each party its own output. Server and client sdk; runners `wasm`, `python`, `dobj`; `TEE_MODE` from `mock` to real hardware. Outside the node, and npm rather than pnpm | nothing | ### Around it | repo | what | | --- | --- | | [pac-design](https://github.com/project-pac/pac-design) | this site | | [pac-website](https://github.com/project-pac/pac-website) | projectpac.net | | [pac-ci](https://github.com/project-pac/pac-ci) | the composite action that lays the constellation out as siblings in ci, and the install script behind it. Not part of the constellation: not installed, not versioned with it, not in the registry | ## Getting the constellation Tools: Node 24 -- `.nvmrc` in every repository, so `nvm use` reads it; use that major, because `better-sqlite3` is compiled against the Node ABI at install and a newer Node fails every suite that opens a database with a `NODE_MODULE_VERSION` mismatch that reads like a broken change and is not one. pnpm 11.21.0, the `packageManager` in every manifest (`npm i -g pnpm@11.21.0`). `just`; `mprocs` for every `just dev` (`brew install mprocs`); git; curl. jc-tee-vm is npm, not pnpm. ```bash mkdir pac && cd pac for r in pac-node pac-plugins pac-network pac-router pac-flow-template pac-flow-meta pac-ux pac-ci jc-tee-vm; do git clone git@github.com:project-pac/$r.git done sh pac-ci/constellation/install-constellation.sh pac-node pac-plugins pac-network pac-router pac-flow-template pac-flow-meta pac-ux (cd jc-tee-vm && npm install) ``` pac-node installs first, and the rest in the order [`install-constellation.sh`](https://github.com/project-pac/pac-ci/blob/main/constellation/install-constellation.sh) has them: every other checkout resolves `cordis` through pac-node (`overrides` in its `pnpm-workspace.yaml`), because the `Context`/`Events` declaration merge only adds up when every checkout resolves the same copy, and a repository installed before pac-node fails with two copies of the same class. The script runs on a laptop as it does in ci. Name the repositories you cloned and it orders them, refuses a set whose `link:`s point outside it, and installs each with `--frozen-lockfile`; it never clones, and with no arguments it wants all ten present. By hand: `pnpm install` in each, in that order. The rule those manifests follow: a runtime dependency on a sibling names a **published version** -- `"@projectpac/sdk": "0.6.0"` -- redirected to the checkout by an `overrides` entry, because a `link:` survives `pnpm pack` verbatim and a package declaring one cannot be published; a `devDependency` is a `link:` path. A value imported at runtime is a dependency, full stop: a `link:` resolves anything in a checkout and fails on a node that installed from a registry. From there, each repository's README says how its lane runs and each `AGENTS.md` (`CLAUDE.md` is a symlink to it) says what trips people up; `pnpm check` is the gate everywhere and what ci runs. In ci, every repository but pac-node lays its siblings out with [`project-pac/pac-ci/constellation`](https://github.com/project-pac/pac-ci/blob/main/README.md) and a `needs` list that is its hand-written transitive closure.