agentproto

agentproto-apps

Operate and build Agentproto apps — AIP-53 app bundles (`defineApp().emit(dir)`,

agentproto 10 1 Updated 3w ago
GitHub

Install

npx skillscat add agentproto/ts/agentproto-apps

Install via the SkillsCat registry.

SKILL.md

Agentproto apps (AIP-53)

A packaged app is a directory that ships one or more AIP-42 agents, the
AIP-15 workflows they run, and (optionally) a single-file web UI, emitted under
<dir>/.agentproto/ by defineApp().emit(dir). The daemon installs, applies,
runs, and persists them. This skill is the app plane on top of the base
agentproto skill (sessions/drivers live there).

Anatomy — what an app dir contains

<appDir>/
  .agentproto/
    APP.md                            # frontmatter: id, agents[], workflows[], ui{path,tools}
    agents/<id>/AGENT.md              # AIP-42 agent manifests
    workflows/<id>/WORKFLOW.md        # AIP-15 workflow manifests
    ui/index.html                     # single-file dashboard (CSS+JS inline)
  data/                               # default data dir (app_data_* writes here)
<dataDir>/                            # …or wherever `app_install {dataDir}` /
                                      # `app install --data-dir` pointed it

APP.md frontmatter (schema: app/v1, id, agents: [{id,path}],
workflows: [{id,path}], ui: {path, title, tools: [...]}, optional
data: {dir} — the default data dir, relative to the app dir). The ui.tools
array is an allowlist — app_tool_call REFUSES any tool id not in it, so
when the UI calls a new daemon tool (e.g. app_data_read), add it there.

Gotcha: this repo's root .gitignore ignores .agentproto/ by default
(it targets agent-scoped scratch). For a first-class app bundle the
.agentproto/ dir IS the deliverable — git add -f <appDir>/.agentproto to
commit it.

The app lifecycle (daemon verbs)

  • app_install {dir} — validates the app (loadAppHandle), cross-checks
    every WORKFLOW.md tool step id against the daemon's registered tools
    (reports ALL missing at once), and checks the agent adapter resolves. Upserts
    by appId. Re-installing refreshes the record.
  • app_apply {appId, scopeId?, dir?} — makes the app's capabilities
    available in a scope; installs first if dir given and not already installed;
    validates requires deps are applied. Idempotent.
  • app_run {appId, agents?, sequence?, prompt?, cwd?, model?, harness?, adapter?} —
    spawns one live session per agent under a fresh appRunId. agents runs them
    CONCURRENTLY. sequence: [id, ...] runs them ONE-AT-A-TIME in order (each
    awaited to a terminal session state before the next), for scout→tailor-style
    dependency chains — both under one appRunId. Pass model/harness (or
    adapter) to select the runner for every spawn.
  • app_status {appRunId} — returns the run's session descriptors + its
    workflow runs. As of the sequential-run work, app_status LAZILY reconciles:
    if the stored run status is running but every session is terminal, it
    reports ended (with a synthesized endedAt) — so concurrent runs are also
    observed to conclude instead of hanging forever at running.
  • app_stop {appRunId} — kills the run's sessions and marks it terminal.
  • app_list — installed apps with a run-history summary (includes
    adapter/harness/model when set on the run).
  • app_uninstall / app_unapply / app_list_applied / app_catalog /
    app_skill_get / app_artifact_get — installation/uninstall and catalog.

Terminal states: run-level status is running | stopped | ended | failed.
A session is terminal at its descriptor status ∈ ended|exited|done|failed| stopped|killed. The UI's "is it done" check should accept the run wrapper
(ended/done/endedAt) OR any terminal session status — never only the
wrapper, since a completed run may still read running until reconciliation.

The app-scoped data plane (app_data_*)

Generic fs-* tools are workspace-rooted — unsafe/incompatible for
installed-app storage. Apps persist through the app-scoped surface, anchored to
the installed app's data dir (InstalledApp.dataDir, default <dir>/data,
persisted in ~/.agentproto/apps.json) with path-traversal protection:
resolveAppDataPath rejects absolute paths, drive-letter prefixes, and any
../symlink escape; every failure returns an MCP error envelope containing
traversal.

Data dir ≠ source dir. app_install {dir, dataDir?} (CLI: agentproto app install <appDir> --data-dir <path>) points the plane at a directory outside
the source tree — that's where multi-GB generated output belongs. Precedence:
explicit dataDir → the previously installed one (a bare re-install never
moves data) → APP.md data: {dir} (relative to the app dir) → <dir>/data.
app_list shows it. Resolution rule for an app-relative path:

  1. resolves under dataDir;
  2. under the default layout (<dir>/data) a leading data/ is the legacy
    spelling and is dropped — data/trips/x.json ≡ trips/x.json;
  3. if the path (or its top-level folder) exists only under the source dir
    (a pre-dataDir install), it resolves there — reads find it, writes update it
    in place, app_data_list merges both views. New paths land under dataDir.

Address files at the new base (trips/<id>/brief.json, not
data/trips/...). Reserved for later: a store.sqlite in dataDir behind an
app_data_query tool — not there yet.

  • app_data_read {appId, path} → {appId, path, exists, content} —
    .json paths parse content; others return raw text. exists:false when
    absent. Never leaks the host absolute path (returns the relative path).
  • app_data_write {appId, path, content} → {appId, path, size} —
    JSON-stringified (pretty) for .json; {text} otherwise. mkdir -p parent,
    atomic tmp+rename. Best-effort, never crashes.
  • app_data_list {appId, dir?} → {appId, dir, entries:[{name,type,size}]} —
    missing dir → empty entries (not an error).
  • app_data_migrate {appId, force?} → one-time import of a legacy data
    shape into the durable layout; idempotent via data/state.json (force
    re-runs). Reports {migrated, jobCount, dossierCount, skippedFolders, alreadyMigrated?}.

Durable layout convention (adapt to the app; the Job Application Kit uses,
all relative to the data dir):

<dataDir>/jobs/<jobId>.json
<dataDir>/search/<searchRunId>.json
<dataDir>/rankings/<rankingRunId>.json   (+ rankings/latest.json for the list)
<dataDir>/state.json
<dataDir>/applications/<jobId>/{application.json, job.json, cv.json, cover.md, form-answers.md}

Normalize legacy records on migrate: ensure id === jobId (both present) and
derive applyUrl from url when missing. Durable files SURVIVE browser reload;
localStorage is for harmless preferences only, never as the source of truth.

Serving the UI (the bridge)

agentproto app serve <appDir> (or the equivalent in-process host) serves
<appDir>/.agentproto/ui/index.html with a window.McpApp bridge whose
callTool POSTs to the daemon's /mcp endpoint (same-origin JSON
/__agentproto/tool-call route bridged by the serve host). The in-page JS
dispatches through a wrapper:

window.McpApp.connect().then(bridge => {
  callTool = bridge.callTool
  // appData migrate → list → read, then render
})

App UI tool calls go through app_tool_call {appId, tool, args} (enforced by
the ui.tools allowlist), so every tool the UI uses must be allowlisted in
APP.md. If the bridge is absent the UI should fall back to a standalone/mock
mode instead of crashing.

Display mode (the ⤢ toggle)

The injected bridge also mounts a floating fullscreen toggle, placed clear of
the host's chrome (hostContext.safeAreaInsets) and themed from
hostContext.theme. It shows only for modes the host advertises in
hostContext.availableDisplayModes — Claude Desktop advertises none and has
its own native control, so nothing is drawn there; Codex advertises
fullscreen. The bridge logs [mcp-app] displayMode= … availableDisplayModes= …
plus the host-context key names, which is the first thing to read when a
toggle doesn't appear.

The controller is window.McpApp.displayMode (also on the object connect()
resolves to): get(), available(), request(mode), onChange(cb),
mountToggle(el?). An app with its own header opts the floating button out
and places it itself:

<meta name="agentproto-display-toggle" content="none">   <!-- or "optimistic" -->
<script>
  const app = await window.McpApp.connect()   // connect({displayToggle:"none"}) also works
  app.displayMode.mountToggle(document.querySelector("header"))
</script>

optimistic shows the button against a host that advertises nothing but may
still honour the request, and hides it permanently on the first refusal.
Restyle via --agentproto-dm-bg/-fg/-border/-top/-right.

Recipe — build/wire an app UI against durable data (minimal)

  1. Author the app bundle under <appDir>/.agentproto/ (APP.md + agents +
    workflows + ui/index.html). Use defineApp().emit(dir) if authoring in TS.
  2. app_install {dir} then app_apply {appId}.
  3. In the UI, on bridge connect, call app_data_migrate {appId} once, then
    app_data_list/app_data_read to render datasets from disk (NOT
    localStorage). Allowlist app_data_* in APP.md ui.tools.
  4. Wire user-facing model/harness pickers into app_run args (model,
    harness) — they're mirrored onto the run and observable via app_list/
    app_status.
  5. For scout→tailor: call app_run {sequence:["scout","tailor"], model, harness}
    and poll app_status until ended (or any session terminal).

Test an installed app (the smoke path)

  1. Serve it: agentproto app serve <appDir> and open the printed URL.
  2. Fresh load runs app_data_migrate once → dashboard populates from
    data/... on disk — confirms recovery/migration (not a zero dashboard).
  3. From devtools assert path-traversal is rejected:
    callTool("app_tool_call",{appId,tool:"app_data_read",args:{path:"../etc/passwd"}}) → traversal error.
  4. Assert the model/harness controls surface and that a Full Pipeline run
    reaches a terminal ended state (not stuck running).
  5. Headless: app_data_migrate/app_data_* are covered by
    packages/runtime/src/__tests__/app-data.test.ts; app_run sequencing +
    terminal reconciliation by app-tools.test.ts. Run
    pnpm --filter @agentproto/runtime test.

Do / don't

  • Do persist via app_data_*; path-traversal it.
  • Do keep UI tool calls allowlisted in APP.md.
  • Do treat a run done when wrapper OR any session is terminal.
  • Don't treat app_data_* like generic fs-tools (they're app-scoped).
  • Don't use localStorage as the data source of truth.
  • Don't run app_run agents concurrently when the flow is dependency-ordered
    — use sequence.