PKU harness
Concepts

Architecture

The PKU harness core is an agent loop, a design system and a set of host services. Every feature comes from a plugin, and all plugins use the same interface.

The design of PKU harness borrows two ideas from DeepSeek Harness. First, the app's features all come from plugins. Second, the app is built around an AI agent, but you and the agent can do exactly the same things.

To achieve the second idea, each feature is written once, as one typed tool. When you tap a button, the screen calls the tool. When you ask in a conversation, the agent calls the same tool.

Layers

The app's code has four layers. From top to bottom they are the UI, the core runtime, the plugins and the native modules.

UI            routes · screens · design system (tokens, components, motion) · generative-UI catalog
                   │
Core runtime  plugin registry + permissions · agent loop · dashboard · scheduler · storage
              campus session manager (IAAA, per-service sessions)
                   │
Plugins       PLUGIN.md documents (interpreted)  ·  bundled TypeScript  ·  remote MCP  ·  providers
                   │
Native        campus-http module (redirect control, per-session cookie jars, HTTP/1.1, no proxy)

Dependencies only point downwards. Plugins never import app code. When a plugin needs to do anything outside itself, it goes through host services (ctx.services). When it needs to show UI, it goes through HostUI.

Parts of the harness

We call the layer beneath the plugins the harness. It has four parts.

PartInterface for plugins
Agent looptools, instructions, context, contributes.commands
Design systemHostUI (host.components, host.useTheme() and so on)
Host servicesHostServices (ctx.services, props.services)
Generative UIThe ui.render tool, HostUI.components.GenUI

The agent loop talks to the model. It is built on the AI SDK, and the model comes from any provider plugin. With each request it gives the model every enabled tool and receives the answer as a stream. The model can call tools in as many steps as the task needs; a reply has no step limit and ends when the model answers or you stop it. When the history grows past the context budget during a reply, the loop summarizes the older steps and carries on. It also keeps the history the model can see for each conversation and records token usage. Scheduled agent runs, which nobody watches, keep a small step limit.

The design system contains design tokens, native controls and motion. Plugin screens are built from it, so they look like the app's own screens. They also keep working when the app changes.

The host services are the services the app offers to plugins. They cover campus HTTP requests, the scheduler, widgets, to-dos, the timetable, notification readiness, files, the browser, cryptography, the plugin registry and the settings registry.

Generative UI is a catalog of components. The agent writes a JSON description, and the app shows the matching components in the chat or on the dashboard.

The agent operates everything

Anything you can do in the app, the agent can do through a tool. The screens are a second way into the same stored data, never a separate path. Three surfaces cover the whole app.

SurfaceWhat it operatesThrough
Campus and dataCampus services, the card, the schedule, to-dos, the dashboard, memory, attached filesEach plugin's tools
The app itselfEvery setting: appearance, language, time zone, haptics, the AI and voice services, each plugin's switch and permissions, each plugin's own preferencesThe settings registry and the core settings plugin
The phoneOther apps on the deviceCopilot (copilot.* tools)

The settings registry is one list of typed entries, each with a key, a group, a title, a kind (choice, boolean, text or list), its options and its current value. It holds:

  • the host's own settings: app.theme, app.language, app.timezone, app.haptics, ai.provider, ai.model and voice.provider;
  • for each plugin, plugin.<id>.enabled (not for core plugins) and plugin.<id>.permission.<effect>, set to auto, ask or off;
  • each plugin's own preferences, declared in its manifest as PluginDef.preferences and listed as <plugin>.<name>, for example copilot.enabled, copilot.apps and copilot.screenshots.

A plugin adds a setting by declaring it; nothing in the host changes. Entries marked uiOnly, such as API keys, the campus account and system notification permissions, are listed with the screen where they live. The agent can't set them; it opens that screen for the user.

The core settings plugin gives the agent three tools: list (read), change (write) and open. Changes follow the normal permission modes, refuse unknown and uiOnly keys before anything changes, validate each value against its entry, and return each key's value before and after. Plugins reach the same registry through HostServices.settings.

Who provides the four tabs

TabProvided by
ChatThe harness
DashboardThe dashboard plugin
ScheduleThe schedule plugin
SettingsThe harness, with some rows contributed by plugins

In the harness, the Dashboard and Schedule tabs are empty places that a plugin fills. We call such a place a slot. Plugins fill slots through contributes.tabs.

Plugins

A plugin takes one of three forms:

  • a plugin document, which is a PLUGIN.md file that the app interprets;
  • a bundled plugin, which is a TypeScript object built with definePlugin that ships with the app;
  • a remote plugin, which is a Model Context Protocol (MCP) server.

The harness turns every form into the same structure: a set of tools, plus optional UI. Each tool has an id of the form <plugin>.<tool>, an effect and an input schema.

Here is the smallest useful bundled plugin. It has a single tool, greet, which says hello.

import { definePlugin, defineTool } from '@pku-harness/sdk';
import { z } from 'zod';

export const helloPlugin = definePlugin({
  id: 'hello',
  name: '你好',
  description: '演示插件。',
  version: '0.1.0',
  scopes: [],
  tools: {
    greet: defineTool({
      title: '打招呼',
      description: 'Greet someone by name.',
      input: z.object({ name: z.string() }),
      effect: 'read',
      execute: async ({ name }) => ({ greeting: `你好,${name}` }),
    }),
  },
});

The same plugin can also be written as a plugin document: a PLUGIN.md with tools: { greet: … }, where the tool's logic is written as YAML steps. See Declarative plugins. Plugins compares the three forms.

Where data lives

DataWhere
Campus credentials, model and voice-input API keys, remote plugin tokensThe phone's keystore (SecureStore)
Chat sessions, usage, widgets, scheduled jobs, to-dos, timetable, plugin key-value storageA SQLite database on the phone
Plugin data you chose to store onlineA PKU harness Cloud provider, end-to-end encrypted

By default, no data passes through any server run by the project. The model provider you set up receives the conversation. Campus services receive the same requests that a browser would send.

How updates reach users

Different kinds of code are updated in different ways.

What changedHow it reaches users
A plugin document (PLUGIN.md), including the official ones bundled with the appThe app fetches its link again
A remote pluginIts server is updated; the app calls it live
A marketplace listing (registry.json)The app fetches it live
App JavaScript, the SDK, bundled TypeScript pluginsA signed over-the-air (OTA) update, or a new app release
Native code (native modules, system permissions)A new app release

The app fetches a plugin document's link again at most once a day. If the new version declares the same access, it takes effect at once. If it asks for wider access, it waits until you confirm. Plugin documents never use OTA updates.

Over-the-air updates come from our own update server and apply the next time the app starts. They use expo-updates with a static, code-signed server (https://pkuharness.com/updates/). Continuous integration (CI) signs every update with a key that only the maintainers hold. The app contains the matching certificate and rejects updates from anywhere else.

runtimeVersion uses the fingerprint policy. This means that any change to native code changes the runtime version. Older APKs then stop receiving new OTA updates, and users need to install a new APK.

Further reading

  • Plugins: plugin documents, bundled plugins and remote plugins, and how to choose between them.
  • Tools and effects: the one way a plugin offers features.
  • Security model: credentials, prompt injection and consent.

On this page