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.
| Part | Interface for plugins |
|---|---|
| Agent loop | tools, instructions, context, contributes.commands |
| Design system | HostUI (host.components, host.useTheme() and so on) |
| Host services | HostServices (ctx.services, props.services) |
| Generative UI | The 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.
| Surface | What it operates | Through |
|---|---|---|
| Campus and data | Campus services, the card, the schedule, to-dos, the dashboard, memory, attached files | Each plugin's tools |
| The app itself | Every setting: appearance, language, time zone, haptics, the AI and voice services, each plugin's switch and permissions, each plugin's own preferences | The settings registry and the core settings plugin |
| The phone | Other apps on the device | Copilot (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.modelandvoice.provider; - for each plugin,
plugin.<id>.enabled(not for core plugins) andplugin.<id>.permission.<effect>, set toauto,askoroff; - each plugin's own preferences, declared in its manifest as
PluginDef.preferencesand listed as<plugin>.<name>, for examplecopilot.enabled,copilot.appsandcopilot.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
| Tab | Provided by |
|---|---|
| Chat | The harness |
| Dashboard | The dashboard plugin |
| Schedule | The schedule plugin |
| Settings | The 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.mdfile that the app interprets; - a bundled plugin, which is a TypeScript object built with
definePluginthat 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
| Data | Where |
|---|---|
| Campus credentials, model and voice-input API keys, remote plugin tokens | The phone's keystore (SecureStore) |
| Chat sessions, usage, widgets, scheduled jobs, to-dos, timetable, plugin key-value storage | A SQLite database on the phone |
| Plugin data you chose to store online | A 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 changed | How it reaches users |
|---|---|
A plugin document (PLUGIN.md), including the official ones bundled with the app | The app fetches its link again |
| A remote plugin | Its server is updated; the app calls it live |
A marketplace listing (registry.json) | The app fetches it live |
| App JavaScript, the SDK, bundled TypeScript plugins | A 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.
Troubleshooting
Common problems with PKU harness and how to solve them — the campus network, verification codes at sign-in, AI provider and voice input errors, attached files, late reminders, app updates, the payment code and Copilot setup.
Plugins
Plugins come in three kinds. Plugin documents (PLUGIN.md) are interpreted by the app and installed by link. Bundled TypeScript plugins ship inside the app. Remote plugins are MCP servers added by URL. All three share one registry, one permission system and one agent loop.