From e94a928cf77e20d6dba24f57262d11059e28c64e Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 30 Sep 2026 16:45:49 -0700 Subject: [PATCH 1/4] feat(skills): add an interchange skill chain with agents, workflows, run modes, and client apps --- docs/PRODUCT.md | 2 +- docs/TELEMETRY.md | 2 +- plugins/corbits-skills/manifest.json | 2 +- .../skills/interchange-agents/SKILL.md | 100 ++++++++++++++++++ .../skills/interchange-client-apps/SKILL.md | 69 ++++++++++++ .../skills/interchange-run-modes/SKILL.md | 58 ++++++++++ .../skills/interchange-workflows/SKILL.md | 93 ++++++++++++++++ .../skills/interchange/SKILL.md | 41 +++++++ src/telemetry/classify.ts | 5 + 9 files changed, 369 insertions(+), 3 deletions(-) create mode 100644 plugins/corbits-skills/skills/interchange-agents/SKILL.md create mode 100644 plugins/corbits-skills/skills/interchange-client-apps/SKILL.md create mode 100644 plugins/corbits-skills/skills/interchange-run-modes/SKILL.md create mode 100644 plugins/corbits-skills/skills/interchange-workflows/SKILL.md create mode 100644 plugins/corbits-skills/skills/interchange/SKILL.md diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 3650c8727..15eb26afc 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -107,7 +107,7 @@ the file path and parse details. The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** or `/connect` adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/compact` (fold conversation context now, optional trailing instructions to the summarizer; does not wait for the 60% occupancy governor; idle success shows the fold and does not start a new turn), `/mcp` (enable, disable, or remove servers), `/handoff [optional instructions]` (folds context through the shared operator pipeline, then immediately starts the next turn with the instructions as the inbound content — default copy when omitted; unlike `/compact`, which stops after the fold, handoff always re-infers, so the operator can pivot goals without `/clear`; a handoff issued mid-tool-batch queues behind the in-flight batch and whichever boundary fires first runs the single fold), and `/yolo` (`/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/` command per available workflow. `/yolo` persists skip-permissions to the active settings file. That file is the user-global `~/.corbits/settings.json` by default, making the setting machine-wide; explicit `--config ` selects a different active file, and `/yolo` writes that file. An ordinary TUI launch without the same `--config` returns to the user-global source and does not modify the custom file. `--dangerously-skip-permissions` and `--yolo` are process-only aliases. Secret-guard and authz still apply. When a session starts with its active persisted setting already on, the TUI and `corbits exec` warn that permission prompts are disabled by saved settings at the active settings path and direct the operator to edit that file to re-enable them. Plugins can register additional commands. -**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request` finds or opens the PR for the current branch, and `/review` takes a PR target and reviews it from a worktree; `/docs` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` takes a plan and a ticket to a pushed branch (worktree, per-commit planner/build/reviewer loop, whole-branch review, push, hand off to `/pull-request`) — it does not steal planning from `/plan`. Substantial coder work consumes a planner / `/plan` plan first; tiny parent-DIY stays plan-optional. `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/issue` finds or creates the tracker issue: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). There is no first-party dispatch skill: the dispatch director orchestrates natively. `typescript` stays `use_skill` only (`user-invocable: false`); `git-worktrees` is a background library that is not listed for `use_skill`. Designer and the other specialists are not slashes; they remain closed directors via `spawn_agent(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `skill_search` (descriptions) then `use_skill` (body). Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone. +**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. Background reference skills (`typescript`, `git-worktrees`, and the `interchange` hub with its `interchange-agents`, `interchange-workflows`, `interchange-run-modes`, and `interchange-client-apps` references) have no slash; the model loads them with `skill_search` and `use_skill`, and each one ends with "need more, go here" pointers to the next skill in the chain. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request` finds or opens the PR for the current branch, and `/review` takes a PR target and reviews it from a worktree; `/docs` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` takes a plan and a ticket to a pushed branch (worktree, per-commit planner/build/reviewer loop, whole-branch review, push, hand off to `/pull-request`) — it does not steal planning from `/plan`. Substantial coder work consumes a planner / `/plan` plan first; tiny parent-DIY stays plan-optional. `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/issue` finds or creates the tracker issue: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). There is no first-party dispatch skill: the dispatch director orchestrates natively. `typescript` stays `use_skill` only (`user-invocable: false`); `git-worktrees` is a background library that is not listed for `use_skill`. Designer and the other specialists are not slashes; they remain closed directors via `spawn_agent(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `skill_search` (descriptions) then `use_skill` (body). Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone. Providers are **models-first**: there is no standalone `/login` command. `/model` opens a **models-only list** (Recent, Favorites, then connected provider/model rows) — type-to-filter owns printable keys, so Connect is never a bare letter. **Alt+A** or `/connect` opens a dedicated add-provider selector over every first-class kind (OpenAI dual-path ChatGPT OAuth or API key, xAI, OpenCode Zen, Anthropic, Google, OpenCode Go, Z.AI Coding Plan, Ollama, Custom), each annotated with its live account count and never filtered out for “already connected.” **Alt+F** toggles favorite on the highlighted model. **Alt+D** persists the highlighted pair as the default without switching the live session. Advanced provider drill-down (edit/delete/tiers) stays on the advanced surface, not a bare printable key while the model list is filtering. OAuth providers open their existing browser login with a named account step so multiple accounts per kind coexist (`codex/work`, …). API-key providers use the same named-instance step before the key (auth-only form: instance name + key + fixed catalog base URL), so personal and team keys land as distinct catalog rows (`openai/default`, `anthropic/work`, …); reusing a name re-keys that instance after confirm. Custom remains a free-form single endpoint (full manual form). Successful connect refreshes the catalog and reopens the model list focused on the new account’s default model. OpenCode Go lists models from the live `/zen/go/v1/models` catalog (packaged seed on fetch failure), routes each by its protocol metadata (chat completions, OpenAI responses, or Anthropic messages) and can show subscription usage in the status bar when active (rolling 5h / weekly / monthly windows when the usage API responds; omitted on auth or network failure). When Go returns a quota or rate-limit error — including some HTTP 400 responses that carry limit payloads — Corbits classifies them so quota aborts cleanly and short provider rate limits remain retryable. On a free-tier or subscription quota hit, wait for the window to reset or use OpenCode Zen free models. diff --git a/docs/TELEMETRY.md b/docs/TELEMETRY.md index a31d120c3..44bf126c5 100644 --- a/docs/TELEMETRY.md +++ b/docs/TELEMETRY.md @@ -81,7 +81,7 @@ director ids from `DIRECTOR_IDS` (and the legacy `worker` alias) are reported by id; project-defined or marketplace profile ids become `custom`. `skill_used` carries `skill_name`: a first-party skill name reportable by name from the closed `corbits-skills` allowlist in `src/telemetry/classify.ts` -(the slash workflows plus `git-worktrees`, `typescript`), or `custom` for anything else, including bundled background +(the slash workflows plus the background skills `git-worktrees`, `typescript`, and the `interchange` chain), or `custom` for anything else, including bundled background skills outside the list. Unknown, project-local, and plugin-authored skill names are never transmitted; `skill_name` is the only identifying-adjacent property the event can carry. diff --git a/plugins/corbits-skills/manifest.json b/plugins/corbits-skills/manifest.json index 323880d39..791e85f45 100644 --- a/plugins/corbits-skills/manifest.json +++ b/plugins/corbits-skills/manifest.json @@ -3,5 +3,5 @@ "name": "Corbits Skills", "kind": "command", "defaultEnabled": true, - "description": "Default operator skills and slash commands (implement, refactor, review, pull-request, issue, docs, interview, plan)." + "description": "Default operator skills and slash commands (implement, refactor, review, pull-request, issue, docs, interview, plan) and background reference skills (git-worktrees, typescript, interchange)." } diff --git a/plugins/corbits-skills/skills/interchange-agents/SKILL.md b/plugins/corbits-skills/skills/interchange-agents/SKILL.md new file mode 100644 index 000000000..4f9147140 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-agents/SKILL.md @@ -0,0 +1,100 @@ +--- +name: interchange-agents +description: Build and embed an Interchange agent in code with tools, inference sources, and tests. Load when writing createAgent or defineAgent code. +user-invocable: false +--- + +# Interchange agents + +Use `@intx/agent` when you want an agent inside your own program, CLI, or service. No hub is required. + +## Minimal agent + +```ts +import { + createAgent, + createDefaultDirectorRegistry, + createStaticCredentialResolver, + defineAgent, + type BaseEnv, +} from "@intx/agent"; +import { noopAuditStore, permissiveAuthorize } from "@intx/agent/testing"; +import { createIsogitStore } from "@intx/storage-isogit/node"; + +const storage = await createIsogitStore(contextDir); // directory must exist +const def = defineAgent({ + id: "my-agent", + systemPrompt: "You are a helpful assistant.", + tools: [], + capabilities: [], + inference: { sources: [{ provider: source.provider, model: source.model }] }, +}); +const env: BaseEnv = { + sources: [source], // { id, provider, baseURL, credentialId, model } + defaultSource: source.id, + storage, + workdir: contextDir, + audit: noopAuditStore(), + authorize: permissiveAuthorize(), + directors: createDefaultDirectorRegistry(), + readCurrentMaterial: createStaticCredentialResolver({ + [source.credentialId]: apiKey, + }), +}; +const agent = await createAgent(def, env); +try { + const result = await agent.send(prompt); + if (result.type !== "reply") + throw new Error(`suspended on ${result.correlationId}`); + console.log(result.reply); +} finally { + await agent.close(); +} +``` + +- `send()` returns a union. Anything other than `reply` means the agent suspended on a gate (approval, pending tool) and needs an inbound message with that `correlationId`. +- `permissiveAuthorize` and `noopAuditStore` are test stubs. A product supplies a real `authorize` built on `@intx/authz` and a real audit store. +- Re-running on the same `contextDir` resumes the conversation. Clone the directory and reset `HEAD` to rewind. + +## Tools + +```ts +import { defineTool } from "@intx/agent"; +import { posix, type PosixToolEnv } from "@intx/tools-posix/sidecar-bundle"; + +let disposeTools: (() => Promise) | undefined; +const posixTools = defineTool({ + id: posix.id, + requires: posix.requires, + definitions: posix.definitions, + factory: (env) => { + const b = posix(env); + disposeTools = b.dispose; + return b; + }, +}); +// def.tools = [posixTools]; env gains toolCwd (tree the model edits) +``` + +`toolCwd` is what the model reads and edits. `workdir` is the isogit storage boundary. They are different. Tool bundle lifetimes belong to the caller: dispose on close and on failed `createAgent`. LSP is a plugin passed as `{ ...env, plugins: [lsp(env)] }`. A tool can return a `pendingMarker` to open a correlation gate. + +## Inference + +- List several `sources` for failover across providers and protocols. `agent.setSource()` hot-swaps without interrupting a call. +- Extra providers (OpenAI Responses, Codex, xAI, Ollama) come from Corbits packages: `use_skill corbits-inference`. +- Oversize tool output spills to a blob with a `tool-output://` URI (`sizeCapMaxChars`). + +## Test + +Use `@intx/inference-testing` for scripted replies and to swap `fetch`. Assert on tool_result content, not just absence of crashes. + +## Examples to copy (upstream `examples/`) + +`agent-quickstart` (start here), `coding-agent` (posix plus LSP), `agent-resume`, `agent-rewind`, `agent-audit-log`, `agent-blob-spill`, `agent-rich-tool`, `agent-structured-payload`, `agent-multi-provider`, `agent-gemini-image`, `posix-demo` and `ring-demo` (multi-agent mail). Run with `bun --conditions=intx-src run src/cli.ts`. + +## Need more + +- Reactor, directors, compaction, failover: upstream `docs/INFERENCE.md`. +- Durable multi-step work: `use_skill interchange-workflows`. +- Running it somewhere other than your process: `use_skill interchange-run-modes`. +- Back to the overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md b/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md new file mode 100644 index 000000000..9f730ba98 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md @@ -0,0 +1,69 @@ +--- +name: interchange-client-apps +description: Build a web or mobile client that deploys, triggers, and watches workflows on an Interchange hub with @intx/hub-client. +user-invocable: false +--- + +# Interchange client apps + +A client-driven app talks to a hub over HTTP. The hub owns tenants, grants, credentials, and deploys. The client never talks to a sidecar. + +## Pieces + +- `@intx/hub-client`: browser-side `Transport`, run session, and workflow helpers. It is side-effect free at import. `apps/admin-ui` upstream is the reference client. +- `@intx/hub-api`: the hub's Hono app. Mount extra routes on the same auth stack with `createRequireGrant`. +- `@intx/hub-agent` is sidecar-side, not a client SDK. +- API reference: the hub serves `GET /openapi.json`, and upstream `docs/API.md` is generated. + +## Auth and URLs + +Sign in with better-auth (cookie session, same origin). Tenant is always in the path: `/api/tenants/:tenantId/...`. Cross-tenant user views live under `/api/me/...`. Scripts that need git access use hub-issued `itx_pat_*` or `itx_svc_*` tokens. + +## Deploy, trigger, watch + +```ts +import { + createBrowserTransport, + createRunSession, + deployWorkflow, + triggerWorkflowRun, + deliverWorkflowSignal, + findAwaitingSignal, +} from "@intx/hub-client"; + +const transport = createBrowserTransport(); +const deployment = await deployWorkflow(transport, tenantId, { + source, + entry: "./src/workflow.ts", + sourceOfferingIds: [offeringId], + defaultSourceOfferingId: offeringId, +}); +await triggerWorkflowRun(transport, tenantId, runId, { content: "start" }); + +const session = createRunSession({ + tenantId, + runId, + transport, + onChange: () => render(session.events), +}); +const stop = session.start(); // polls events every 2s until terminal +// when findAwaitingSignal(session.events) returns a signal, answer it: +// await deliverWorkflowSignal(transport, tenantId, runId, { runId, signalName, signalId, payload }) +``` + +- Sources are `asset-source`, `asset-tarball`, or `registry` (registry needs `pin: "name@range"`). +- `signalId` must be stable so retries dedupe. A terminal run cannot be retriggered. +- Errors throw `ApiError(status, code, message)`. Validate every response with arktype. +- Only a browser transport ships. A non-browser client must supply its own `Transport` that adds credentials. +- Live updates: `Transport.subscribe` uses SSE, but the shipped run session polls. +- Approvals: `GET /api/tenants/:t/approvals` with approve and reject POSTs. The docs disagree on whether these are live, so verify in the code. + +## UI + +Chat, run, approval, and step components exist in `@corbits/react-ui`: `use_skill corbits-ui-apps`. Scheduled and webhook runs: `use_skill corbits-hub-libs`. + +## Need more + +- Run topology and credentials: `use_skill interchange-run-modes`. +- Workflow authoring: `use_skill interchange-workflows`. +- Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md b/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md new file mode 100644 index 000000000..bff29d921 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md @@ -0,0 +1,58 @@ +--- +name: interchange-run-modes +description: Pick where an Interchange agent runs (in-process, local hub, remote hub plus sidecar) and how sidecars, credentials, and placement work. +user-invocable: false +--- + +# Interchange run modes + +Pick the smallest mode that meets the need. + +| Situation | Mode | +| ----------------------------------------------------- | -------------------------------------------------------------- | +| One agent inside your own CLI or app, one user | In-process `createAgent` with an isogit `contextDir` | +| Coding agent over a local tree | In-process plus `@intx/tools-posix` (and LSP), set `toolCwd` | +| Test a workflow | `runLocal` with scripted inference | +| Durable steps, crash resume, approvals, audit | Hub plus sidecar | +| Multiple users or tenants, grants, pushed credentials | Hub required | +| Host behind NAT, on a laptop, or firewalled | Sidecar dials out to the hub | +| "Desktop" personal agent | In-process, or a local hub that spawns a local sidecar process | +| Untrusted workload | Provisioner that declares an `isolation:*` capability | + +There is no dedicated desktop harness in the repo. Desktop means the local-process mode. Container, VM, Workers, and browser hosts are in progress, not shipped. + +## In-process (no hub) + +See `use_skill interchange-agents`. There is no tenancy, remote credential push, or multi-user auth. You supply `authorize` and audit yourself. + +## Hub plus sidecar + +- The hub is the control plane (Postgres, tenants, grants, credentials, deploys, git). A sidecar is a host process that connects outbound over one WebSocket (`HUB_WS_URL`), registers with an allocation token, then runs each deployment as a supervised child (`bin/workflow-child`). It needs no public address. Use `wss://` off loopback. +- Sidecar env: `SIDECAR_DATA_DIR`, `HUB_WS_URL`, `SIDECAR_ID`, `SIDECAR_TOKEN`, `SIDECAR_CREDENTIAL_ENCRYPTION_KEY`. +- Sidecars exist only through a `SidecarProvisioner` (`ensure` and `destroy`, idempotent, generation-fenced). Inject them with `createHubServer({ sidecarProvisioners, probeSidecarProvisioners })`. None is registered by default, so a fresh dev hub cannot probe or deploy. +- A local-process provisioner just spawns `apps/sidecar/src/index.ts` with those env vars. The reference is `tests/admin-ui-e2e/harness/local-process-sidecar-provisioner.ts` upstream. +- A crashed child is respawned with backoff (1 s doubling to 30 s). Three exits in 60 s latch `crash-looping` and fail the run. +- Dev stack: `bin/db-reset && bin/dev --seed` gives the hub on :3000 and the admin UI on :5173. + +## Credentials + +Agents never discover credentials. The hub resolves them at launch and the harness mediates use. Inference keys travel inside the `agent.deploy` frame, and rotation pushes `sources.update`. Tool credentials are tenant-owned, decrypted on the hub, and delivered over the control channel. Proactive OAuth refresh is not built. Docs disagree on whether keys sit in `deployment.json`, so check `DEV.md` and the code before relying on either. + +## Placement + +A workflow declares what it needs, and a provisioner advertises what it offers: + +```ts +sidecarPlacement: { + capabilities: [{ capability: "platform:ios", effect: "require" }]; +} +``` + +Selectors are exact, `ns:*`, or `*`. Effects are `require` or `block`, and block wins ties. Tenant policy can add constraints that workflows cannot weaken. The OS or container sandbox boundary is not built, so grants are not a syscall or network sandbox. + +## Need more + +- Upstream docs: `HARNESS_DESIGN.md`, `SIDECAR_PLACEMENT.md`, `CREDENTIALS.md`, `AUTH.md`, `GIT_ACCESS.md`, `unified-execution-host-design.md`. +- Client app driving a hub: `use_skill interchange-client-apps`. +- Workflow authoring: `use_skill interchange-workflows`. +- Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange-workflows/SKILL.md b/plugins/corbits-skills/skills/interchange-workflows/SKILL.md new file mode 100644 index 000000000..cd631b892 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-workflows/SKILL.md @@ -0,0 +1,93 @@ +--- +name: interchange-workflows +description: Author, test, and deploy Interchange workflows (defineWorkflow, step, loop, action). Load when writing or debugging @intx/workflow code. +user-invocable: false +--- + +# Interchange workflows + +A workflow is a DAG of steps that the runtime schedules, checkpoints to an append-only event log, and resumes after a crash. The definition is data, so the deploy system can hash it, show its grants, and freeze what was approved. + +## Shape + +```ts +import { defineAgent } from "@intx/agent"; +import { action, defineWorkflow, escalation, loop, step } from "@intx/workflow"; + +const pass = defineWorkflow({ + // loop body: keep it module-private + id: "revision-pass", + trigger: { type: "manual" }, + steps: { + shorten: step({ agent: editorAgent, input: { from: "trigger.payload" } }), + }, +}); + +export const workflow = defineWorkflow({ + // the ONE exported definition + id: "tagline-review", + trigger: { type: "manual" }, + steps: { + revise: loop({ + body: pass, + while: "stillTooLong", + carry: "nextPass", + input: { from: "trigger.payload" }, + maxIterations: 4, + onExhausted: "giveUp", + }), + publish: action({ + handler: "publishTagline", + effect: { requires: ["fs:write"] }, + input: { from: "steps.revise.output.final.shorten" }, + after: ["revise"], + }), + giveUp: escalation({ to: "editor@example.com", after: ["revise"] }), + }, +}); +``` + +Primitives: `step`, `map`, `gate`, `awaitSignal`, `sleep`, `childWorkflow`, `escalation`, `action`, `loop`, `onTrigger`. Triggers are `mail` and `manual` (`schedule` is rejected; use `@corbits/cron`). Inputs use selectors such as `{ from: "steps..output." }`. + +The package manifest names the modules: `"interchange": { "workflow": "./src/workflow.ts", "loops": "./src/loops.ts", "actions": "./src/actions.ts" }`. Loop functions and action handlers resolve by export name. + +## Rules that bite + +- The workflow module exports exactly one definition. +- Agent step output is `{ reply, turn }`. Parse structured output in an `action`. +- Actions get no default input, and handlers get only `(input, ctx, signal)`. Do every effect through `ctx.perform({ effectId, capability, run })`, keep it idempotent, and list the capability in `effect.requires`. +- On convergence a loop breaks before `carry`: `carry` is the last iteration's input, `final` is its output. +- Untimed parks (`awaitSignal` with no timeout, tools needing approval) are refused under a `childWorkflow`. +- Declare needed grants with `grantRequirements` (`source: "creator"` or `"invoker"`). + +## Test locally + +```ts +const run = runLocal(workflow, { + authorize: allowAll, + hasUpstreamSignalResolver: true, + triggerPayload, + loopFns, + actionResolver, + invokeStep: createAgentStepInvoker({ + source, + material, + contextDir, + authorize: allowAll, + }), +}); +const result = await run.complete; // { runId, terminalStatus, outputs } +``` + +`runLocal` is in memory. Its default step invoker runs no inference, so pass your own or script replies with `@intx/inference-testing`. + +## Deploy + +`POST /api/tenants/:t/workflows/deployments` installs, probes (on an airlocked sidecar), gates on the operator's approval set, freezes, then provisions a sidecar. Trigger with `/:runId/mail`, answer waits with `/:runId/signals`. The in-tree hub registers no provisioner, so it cannot deploy until one is injected: `use_skill interchange-run-modes`. + +## Need more + +- Full pitfall list: upstream `docs/WORKFLOW_AUTHORING.md` and `examples/workflow-quickstart`. +- Driving deploys from an app: `use_skill interchange-client-apps`. +- Scheduled or webhook-triggered runs: `use_skill corbits-hub-libs`. +- Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange/SKILL.md b/plugins/corbits-skills/skills/interchange/SKILL.md new file mode 100644 index 000000000..497a301b0 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange/SKILL.md @@ -0,0 +1,41 @@ +--- +name: interchange +description: What Interchange (@intx/*) is and where to go next. Load before building on, with, or inside Interchange, or when code imports @intx packages. +user-invocable: false +--- + +# Interchange + +Interchange (https://github.com/faremeter/interchange, LGPL-2.1, alpha, v0.4) is an agentic operating system. It runs agents as principals with their own identity, grants, and credentials. One authorization engine (`@intx/authz`) gates both API calls and tool calls. Corbits Code itself is built on it: the `@intx/*` packages are vendored under `vendor/intx-*`. + +Alpha means public APIs can change before 1.0, and some docs describe the intended system, not the shipped one. The source is the truth. Check a package's `exports` before importing a subpath you have not seen used. + +## Mental model + +| Piece | What it is | +| --------------------- | -------------------------------------------------------------------------------------------------------- | +| Principal and grants | Users, agents, and workflow runs are principals. Nothing is allowed without a grant (allow, ask, deny). | +| Agent (`@intx/agent`) | Code-driven runtime over the `@intx/inference` reactor. No hub needed. Context is a git repo. | +| Director | One function `(event, state) -> action` that steers the reactor loop (infer, execute tools, suspend...). | +| Tools | Bundles such as `@intx/tools-posix`, `-lsp`, `-mail`, gated by authz. | +| Workflow | A checkpointed DAG of agent steps, loops, and actions (`@intx/workflow`). | +| Hub | Multi-tenant control plane: tenants, grants, credentials, deploys, git server. | +| Sidecar | Host process that dials out to the hub and runs each deployment as a supervised child. | + +State is git: the audit log is the commit history, resume is re-running on the same directory, rewind is moving `HEAD`. + +## Rules for building on it + +- Look in `@intx/*` before writing infrastructure (logging, authz, inference, tools, state). +- Resolve exactly one copy of each `@intx/*` package. Two copies break `instanceof`. +- Validate input with arktype. No classes, no `any`, no default exports, use `create*` factories. +- The upstream gate is `make all`. Never hand-edit generated `docs/API.md`. + +## Need more, go here + +- Build or embed an agent in code, pick tools, sources, and test it: `use_skill interchange-agents`. +- Multi-step, durable, resumable work: `use_skill interchange-workflows`. +- Where and how it runs (local, hub plus sidecar, NAT, credentials, placement): `use_skill interchange-run-modes`. +- A web or mobile client talking to a hub: `use_skill interchange-client-apps`. +- Corbits packages that plug into it (providers, MCP, cron, memory, UI): `use_skill corbits`. +- Design docs in the upstream repo: `docs/ARCHITECTURE.md`, `INFERENCE.md`, `AUTH.md`, `CREDENTIALS.md`, `HARNESS_DESIGN.md`. Package layout: `LAYOUT.md`. Runnable references: `examples/README.md`. diff --git a/src/telemetry/classify.ts b/src/telemetry/classify.ts index faf49c072..c18356287 100644 --- a/src/telemetry/classify.ts +++ b/src/telemetry/classify.ts @@ -86,6 +86,11 @@ const FIRST_PARTY_SKILL_NAMES: ReadonlySet = new Set([ "docs", "git-worktrees", "implement", + "interchange", + "interchange-agents", + "interchange-client-apps", + "interchange-run-modes", + "interchange-workflows", "interview", "issue", "plan", From 4d46e1424027ba7900e9171db5d4438e0af9a329 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Wed, 30 Sep 2026 17:18:40 -0700 Subject: [PATCH 2/4] feat(skills): add interchange-embed-hub and rewrite client-apps around app-driven setup --- docs/PRODUCT.md | 2 +- .../skills/interchange-client-apps/SKILL.md | 79 +++++++------------ .../skills/interchange-embed-hub/SKILL.md | 51 ++++++++++++ .../skills/interchange-run-modes/SKILL.md | 2 +- .../skills/interchange/SKILL.md | 2 +- src/telemetry/classify.ts | 1 + 6 files changed, 82 insertions(+), 55 deletions(-) create mode 100644 plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 15eb26afc..62994f991 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -107,7 +107,7 @@ the file path and parse details. The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** or `/connect` adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/compact` (fold conversation context now, optional trailing instructions to the summarizer; does not wait for the 60% occupancy governor; idle success shows the fold and does not start a new turn), `/mcp` (enable, disable, or remove servers), `/handoff [optional instructions]` (folds context through the shared operator pipeline, then immediately starts the next turn with the instructions as the inbound content — default copy when omitted; unlike `/compact`, which stops after the fold, handoff always re-infers, so the operator can pivot goals without `/clear`; a handoff issued mid-tool-batch queues behind the in-flight batch and whichever boundary fires first runs the single fold), and `/yolo` (`/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/` command per available workflow. `/yolo` persists skip-permissions to the active settings file. That file is the user-global `~/.corbits/settings.json` by default, making the setting machine-wide; explicit `--config ` selects a different active file, and `/yolo` writes that file. An ordinary TUI launch without the same `--config` returns to the user-global source and does not modify the custom file. `--dangerously-skip-permissions` and `--yolo` are process-only aliases. Secret-guard and authz still apply. When a session starts with its active persisted setting already on, the TUI and `corbits exec` warn that permission prompts are disabled by saved settings at the active settings path and direct the operator to edit that file to re-enable them. Plugins can register additional commands. -**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. Background reference skills (`typescript`, `git-worktrees`, and the `interchange` hub with its `interchange-agents`, `interchange-workflows`, `interchange-run-modes`, and `interchange-client-apps` references) have no slash; the model loads them with `skill_search` and `use_skill`, and each one ends with "need more, go here" pointers to the next skill in the chain. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request` finds or opens the PR for the current branch, and `/review` takes a PR target and reviews it from a worktree; `/docs` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` takes a plan and a ticket to a pushed branch (worktree, per-commit planner/build/reviewer loop, whole-branch review, push, hand off to `/pull-request`) — it does not steal planning from `/plan`. Substantial coder work consumes a planner / `/plan` plan first; tiny parent-DIY stays plan-optional. `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/issue` finds or creates the tracker issue: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). There is no first-party dispatch skill: the dispatch director orchestrates natively. `typescript` stays `use_skill` only (`user-invocable: false`); `git-worktrees` is a background library that is not listed for `use_skill`. Designer and the other specialists are not slashes; they remain closed directors via `spawn_agent(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `skill_search` (descriptions) then `use_skill` (body). Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone. +**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. Background reference skills (`typescript`, `git-worktrees`, and the `interchange` hub with its `interchange-agents`, `interchange-workflows`, `interchange-run-modes`, `interchange-embed-hub`, and `interchange-client-apps` references) have no slash; the model loads them with `skill_search` and `use_skill`, and each one ends with "need more, go here" pointers to the next skill in the chain. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request` finds or opens the PR for the current branch, and `/review` takes a PR target and reviews it from a worktree; `/docs` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` takes a plan and a ticket to a pushed branch (worktree, per-commit planner/build/reviewer loop, whole-branch review, push, hand off to `/pull-request`) — it does not steal planning from `/plan`. Substantial coder work consumes a planner / `/plan` plan first; tiny parent-DIY stays plan-optional. `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/issue` finds or creates the tracker issue: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). There is no first-party dispatch skill: the dispatch director orchestrates natively. `typescript` stays `use_skill` only (`user-invocable: false`); `git-worktrees` is a background library that is not listed for `use_skill`. Designer and the other specialists are not slashes; they remain closed directors via `spawn_agent(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `skill_search` (descriptions) then `use_skill` (body). Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone. Providers are **models-first**: there is no standalone `/login` command. `/model` opens a **models-only list** (Recent, Favorites, then connected provider/model rows) — type-to-filter owns printable keys, so Connect is never a bare letter. **Alt+A** or `/connect` opens a dedicated add-provider selector over every first-class kind (OpenAI dual-path ChatGPT OAuth or API key, xAI, OpenCode Zen, Anthropic, Google, OpenCode Go, Z.AI Coding Plan, Ollama, Custom), each annotated with its live account count and never filtered out for “already connected.” **Alt+F** toggles favorite on the highlighted model. **Alt+D** persists the highlighted pair as the default without switching the live session. Advanced provider drill-down (edit/delete/tiers) stays on the advanced surface, not a bare printable key while the model list is filtering. OAuth providers open their existing browser login with a named account step so multiple accounts per kind coexist (`codex/work`, …). API-key providers use the same named-instance step before the key (auth-only form: instance name + key + fixed catalog base URL), so personal and team keys land as distinct catalog rows (`openai/default`, `anthropic/work`, …); reusing a name re-keys that instance after confirm. Custom remains a free-form single endpoint (full manual form). Successful connect refreshes the catalog and reopens the model list focused on the new account’s default model. OpenCode Go lists models from the live `/zen/go/v1/models` catalog (packaged seed on fetch failure), routes each by its protocol metadata (chat completions, OpenAI responses, or Anthropic messages) and can show subscription usage in the status bar when active (rolling 5h / weekly / monthly windows when the usage API responds; omitted on auth or network failure). When Go returns a quota or rate-limit error — including some HTTP 400 responses that carry limit payloads — Corbits classifies them so quota aborts cleanly and short provider rate limits remain retryable. On a free-tier or subscription quota hit, wait for the window to reset or use OpenCode Zen free models. diff --git a/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md b/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md index 9f730ba98..d5912016a 100644 --- a/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md @@ -1,69 +1,44 @@ --- name: interchange-client-apps -description: Build a web or mobile client that deploys, triggers, and watches workflows on an Interchange hub with @intx/hub-client. +description: Build a client-driven app that sets up its own Interchange hub (tenants, auth, grants, providers, installs, deploys, runs) using stock @intx and @corbits packages. user-invocable: false --- -# Interchange client apps +# Client-driven apps on Interchange -A client-driven app talks to a hub over HTTP. The hub owns tenants, grants, credentials, and deploys. The client never talks to a sidecar. +A client-driven app drives the whole setup. The hub boots vanilla (migrate, mount, serve). The app signs in, creates its tenants, grants, providers, and assets, deploys its workflows, and runs them, all over the hub's HTTP API using stock `@intx/*` and `@corbits/*` packages. Nothing about the product is baked into the hub. -## Pieces +The flagship example is Corbits Solution Builder (`corbitsdev/corbits-solution-builder`). Its architecture note says it directly: the client runs the installer over the hub on first launch and after every credential change. Read `packages/installer/src/*.ts` first. Workbench (`corbitsdev/workbench`) is the server-composition reference. -- `@intx/hub-client`: browser-side `Transport`, run session, and workflow helpers. It is side-effect free at import. `apps/admin-ui` upstream is the reference client. -- `@intx/hub-api`: the hub's Hono app. Mount extra routes on the same auth stack with `createRequireGrant`. -- `@intx/hub-agent` is sidecar-side, not a client SDK. -- API reference: the hub serves `GET /openapi.json`, and upstream `docs/API.md` is generated. +## The shape -## Auth and URLs +- **Host** (once per launch): picks embedded or remote hub, opens the database, migrates, mints keys, mounts the hub, starts sidecar reconcilers. See `use_skill interchange-embed-hub`. +- **Client** (browser or script): everything else, through one `Transport` (`fetch(method, path, body)` plus `subscribe`). +- Keep an `install()` that is idempotent (list-then-create, tolerate 409). Run it on first launch and after changes. Solution Builder keeps it free of any database import. -Sign in with better-auth (cookie session, same origin). Tenant is always in the path: `/api/tenants/:tenantId/...`. Cross-tenant user views live under `/api/me/...`. Scripts that need git access use hub-issued `itx_pat_*` or `itx_svc_*` tokens. +## Bootstrap recipe (client side) -## Deploy, trigger, watch +1. **Auth.** better-auth, email and password. `POST /api/auth/sign-up/email {name,email,password}`, on 422 `POST /api/auth/sign-in/email`. Replay the `better-auth.session_token` cookie. The REST API has no bearer or service token, so an app that acts as "system" holds a user account and keeps its cookie. A desktop app can mint one fixed local owner with a keychain-held password. +2. **Tenant.** `POST /api/tenants {name, slug, parentId?}`. The caller becomes the owner principal with `*`/`*` allow. Find existing ones via `GET /api/me` and `GET /api/me/principals`. A child tenant (`parentId`) per project inherits the parent's catalog, assets, and placement policy. Do not treat the hierarchy as a trust boundary yet: the create route does not appear to check the caller's rights on `parentId` (unverified). +3. **Roles and grants.** `POST /roles`, `POST /grants {roleId|principalId, resource, action, effect, origin}`, `POST /principals/:p/roles/:r`. Model app authorities as grants such as `authority:` / `hold`. An invited member must be PATCHed to `active` before the tenant accepts them. +4. **Inference.** In order: `POST /providers` (set `apiBaseUrl`), `POST /credentials`, `POST /catalog/models`, `POST /catalog/providers` (bound to the credential), `POST /catalog/offerings`. A model provider's credential cannot be repointed, so rotate the same credential id. The offering ids are the failover chain you give each deploy. +5. **Install code.** `POST /assets {kind:"workflow"|"skill"|"package-registry", name}`. Push source through a git token (`POST /git-tokens`, `can_read` and `can_push`, short expiry, revoke after). Poll until the push is visible before deploying. +6. **Deploy.** `deployWorkflow(transport, tenantId, { source: { kind: "asset", assetId, package: { format: "source", commitSha } }, entry, sourceOfferingIds, defaultSourceOfferingId })`. The chain freezes at deploy, so a model switch is a new deployment. A deploy needs a non-empty chain and a sidecar provisioner. +7. **Run.** `triggerWorkflowRun`, then fold `readWorkflowRunEvents` (or `createRunSession`, which polls every 2 s). Answer waits with `deliverWorkflowSignal(transport, tenantId, runId, { runId, signalName, signalId, payload })`. A repeated `signalId` with a different payload is a 409. Resolve tool approvals with `POST /approvals/:id/approve` or `/reject`, which needs `approval:*` / `resolve`. -```ts -import { - createBrowserTransport, - createRunSession, - deployWorkflow, - triggerWorkflowRun, - deliverWorkflowSignal, - findAwaitingSignal, -} from "@intx/hub-client"; +## Rules that save time -const transport = createBrowserTransport(); -const deployment = await deployWorkflow(transport, tenantId, { - source, - entry: "./src/workflow.ts", - sourceOfferingIds: [offeringId], - defaultSourceOfferingId: offeringId, -}); -await triggerWorkflowRun(transport, tenantId, runId, { content: "start" }); - -const session = createRunSession({ - tenantId, - runId, - transport, - onChange: () => render(session.events), -}); -const stop = session.start(); // polls events every 2s until terminal -// when findAwaitingSignal(session.events) returns a signal, answer it: -// await deliverWorkflowSignal(transport, tenantId, runId, { runId, signalName, signalId, payload }) -``` - -- Sources are `asset-source`, `asset-tarball`, or `registry` (registry needs `pin: "name@range"`). -- `signalId` must be stable so retries dedupe. A terminal run cannot be retriggered. -- Errors throw `ApiError(status, code, message)`. Validate every response with arktype. -- Only a browser transport ships. A non-browser client must supply its own `Transport` that adds credentials. -- Live updates: `Transport.subscribe` uses SSE, but the shipped run session polls. -- Approvals: `GET /api/tenants/:t/approvals` with approve and reject POSTs. The docs disagree on whether these are live, so verify in the code. - -## UI - -Chat, run, approval, and step components exist in `@corbits/react-ui`: `use_skill corbits-ui-apps`. Scheduled and webhook runs: `use_skill corbits-hub-libs`. +- Only the browser transport ships. Outside a browser, supply your own: base URL, cookie jar, JSON content type, `ApiError` mapping, empty 202 and 204 bodies. In process you can call `hub.app.fetch(new Request(origin + path))` with the session cookie. +- `@intx/hub-client` covers workflows only. Solution Builder's `installer/src/hub.ts` is the broader typed client for tenants, roles, grants, and the catalog. Validate every response with arktype. +- List endpoints return inherited rows. Filter assets by `tenantId` or you will adopt the parent's same-named asset. Send signals to the tenant the deployment belongs to. +- Deploy lazily, per project or stage, when the work first needs it. Do not provision everything at install. +- Client-side checks (for example guarding which grants the app may mint) are not security. The hub authorizes. +- Never put secrets in responses, logs, or prompts. Credentials are sealed by the hub's cipher. ## Need more -- Run topology and credentials: `use_skill interchange-run-modes`. -- Workflow authoring: `use_skill interchange-workflows`. +- Composing, embedding, and running the hub itself: `use_skill interchange-embed-hub`. +- Sidecars, placement, and credential delivery: `use_skill interchange-run-modes`. +- Workflow code you will deploy: `use_skill interchange-workflows`. +- Corbits libraries to mount (mailbox, artifacts, OAuth login, cron, memory): `use_skill corbits-hub-libs`. UI and reference apps: `use_skill corbits-ui-apps`. - Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md b/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md new file mode 100644 index 000000000..a17768d26 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md @@ -0,0 +1,51 @@ +--- +name: interchange-embed-hub +description: Compose, embed, and run an Interchange hub inside your own app host (database, keys, auth origin, sidecar provisioner, reconcilers, vendoring). +user-invocable: false +--- + +# Embedding an Interchange hub + +Use this when your app owns the hub process: a desktop host, a single binary, or a server that mounts Interchange next to its own routes. For what the app then does over HTTP, see `use_skill interchange-client-apps`. + +## Stock vs composed + +- Upstream `createHubServer` (`apps/hub/src/server.ts`) takes only provisioner and placement options (`sidecarProvisioners`, `probeSidecarProvisioners`, choosers, capability rules). The database, keys, and data dir come from env: `DB_*`, `HUB_DATA_DIR`, `CREDENTIAL_ENCRYPTION_KEY`, `PRINCIPAL_KEY_ENCRYPTION_KEY`, `BETTER_AUTH_BASE_URL`, `BETTER_AUTH_SECRET`. +- An app that needs its own database, auth, or key store copies that composition. Workbench (`apps/hub/src/server.ts`, about 860 lines, real Postgres) and Solution Builder (`packages/embed-hub`, pglite) both do. Expect to re-port when upstream changes. +- The reusable surface is `createApp` and `mountHubRoutes` from `@intx/hub-api` (pass your own `getSession`, which is not tied to better-auth), plus the factories in `@intx/hub-sessions` and `@intx/db`. Mount extra Hono routes (`@corbits/*` libraries) with `createRequireGrant`. + +## The composition, in order + +`createDB` then `createAuth(db)`, credential cipher and principal key store (two distinct 32-byte hex keys), agent repo store (signs with the hub key), asset service, sidecar credential resolver and router, event collectors, session service, sidecar plugin registries (deployment and probe provisioners), workflow allocation and dispatch services, the allocation reconciler, four reconciliation schedulers, then `createApp`. Serve with `Bun.serve({ fetch, websocket })` (the sidecar WebSocket lives at `/api/sidecars/ws`). + +Start the schedulers or allocations never progress. Omit the allocation service and deploys fail with `workflow_provisioning_unavailable`. Omit the cipher and secrets are stored unencrypted with only a warning. + +## Embedded vs remote + +Solution Builder switches on one env var (`_HUB_URL`). Unset, the hub is a Hono app inside the host process and calls go to `hub.app.fetch` with no socket. Set, the host mounts nothing, runs no migrations, and places no sidecars, because a local migration would create a second, divergent control plane. + +## Database and migrations + +- Upstream targets Postgres. `runMigrations(cfg, { schema })` has no ledger and is not idempotent, so guard it (check a table exists) and serialize boots with an advisory lock. The standard flow is `drizzle-kit migrate`. Run each Corbits library's migrations after it. +- pglite is not supported upstream. Solution Builder runs it with one vendor patch (`createDB({ handle, close })`), a schema-bound `drizzle(pglite, { schema })`, a shim that returns rows in postgres.js shape, and its own migration ledger. pglite is single-writer: claim the data dir with a pid file, and never auto-migrate a database stamped by an older revision. + +## Keys and auth origin + +- Mint the credential key, principal key, and hub signing key once and persist them (OS keychain or an owner-only file). Never mint when the store merely failed to answer. Persist the signing key so earlier signed commits still verify. +- Set `BETTER_AUTH_BASE_URL` to the exact origin clients use. The default is `http://localhost:3000`, which makes better-auth reject a loopback sign-up as CSRF. +- Loopback is not authentication. Solution Builder adds a host session token (keychain, exchanged for an HttpOnly cookie) in front of `/api/*`, and lets the sidecar WebSocket bypass it because the hub checks the sidecar's own token. + +## Sidecars + +Sidecars exist only through a `SidecarProvisioner`. Spawn the sidecar entry as a child process with an allowlisted env: `HUB_WS_URL`, `SIDECAR_ID`, `SIDECAR_TOKEN`, `SIDECAR_DATA_DIR`, `SIDECAR_CREDENTIAL_ENCRYPTION_KEY`, optional `SIDECAR_ADAPTER_MANIFEST` (JSON array of `{ provider, specifier, export }`, trusted input that runs code). Write a pid file before anything else, fence by generation, and reap children on shutdown, because sidecars outlive a killed host and dial a hub that is gone. The WebSocket URL is part of the binding fingerprint, so keep the port stable. + +## Consuming Interchange + +Prefer published `@intx/*` packages. Solution Builder vendors the whole upstream workspace (pinned revision, a `PATCHES.md` with a reason and kill date per local edit) because it needs pglite injection, a newer `@intx/workflow`, and an offline compiled binary. Published tarballs carry an `intx-src` export condition that points at source they do not ship, so sidecars and deployed workflows need built `dist/`. Resolve exactly one copy of each `@intx/*` package. + +## Need more + +- What the app does after the hub is up: `use_skill interchange-client-apps`. +- Sidecar protocol, placement, credentials: `use_skill interchange-run-modes`. +- Libraries to mount: `use_skill corbits-hub-libs`. +- Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md b/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md index bff29d921..d7c0c6aea 100644 --- a/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md @@ -53,6 +53,6 @@ Selectors are exact, `ns:*`, or `*`. Effects are `require` or `block`, and block ## Need more - Upstream docs: `HARNESS_DESIGN.md`, `SIDECAR_PLACEMENT.md`, `CREDENTIALS.md`, `AUTH.md`, `GIT_ACCESS.md`, `unified-execution-host-design.md`. -- Client app driving a hub: `use_skill interchange-client-apps`. +- Client app driving a hub: `use_skill interchange-client-apps`. Host embedding a hub: `use_skill interchange-embed-hub`. - Workflow authoring: `use_skill interchange-workflows`. - Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange/SKILL.md b/plugins/corbits-skills/skills/interchange/SKILL.md index 497a301b0..b573ea4fb 100644 --- a/plugins/corbits-skills/skills/interchange/SKILL.md +++ b/plugins/corbits-skills/skills/interchange/SKILL.md @@ -36,6 +36,6 @@ State is git: the audit log is the commit history, resume is re-running on the s - Build or embed an agent in code, pick tools, sources, and test it: `use_skill interchange-agents`. - Multi-step, durable, resumable work: `use_skill interchange-workflows`. - Where and how it runs (local, hub plus sidecar, NAT, credentials, placement): `use_skill interchange-run-modes`. -- A web or mobile client talking to a hub: `use_skill interchange-client-apps`. +- An app that sets up and drives its own hub (tenants, auth, deploys): `use_skill interchange-client-apps`. Hosting the hub in your process: `use_skill interchange-embed-hub`. - Corbits packages that plug into it (providers, MCP, cron, memory, UI): `use_skill corbits`. - Design docs in the upstream repo: `docs/ARCHITECTURE.md`, `INFERENCE.md`, `AUTH.md`, `CREDENTIALS.md`, `HARNESS_DESIGN.md`. Package layout: `LAYOUT.md`. Runnable references: `examples/README.md`. diff --git a/src/telemetry/classify.ts b/src/telemetry/classify.ts index c18356287..5735fd480 100644 --- a/src/telemetry/classify.ts +++ b/src/telemetry/classify.ts @@ -89,6 +89,7 @@ const FIRST_PARTY_SKILL_NAMES: ReadonlySet = new Set([ "interchange", "interchange-agents", "interchange-client-apps", + "interchange-embed-hub", "interchange-run-modes", "interchange-workflows", "interview", From cfc6105f583efe9ca73a0ccbbb8b3b0f60c33ce3 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 1 Oct 2026 06:56:10 -0700 Subject: [PATCH 3/4] docs(skills): frame interchange run modes as a control-plane ladder and add hub sharing, chat, and handoff --- .../skills/interchange-client-apps/SKILL.md | 18 +++++++ .../skills/interchange-embed-hub/SKILL.md | 2 +- .../skills/interchange-run-modes/SKILL.md | 54 +++++++++++++------ .../skills/interchange-workflows/SKILL.md | 17 ++++++ 4 files changed, 75 insertions(+), 16 deletions(-) diff --git a/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md b/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md index d5912016a..baeacf21c 100644 --- a/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md @@ -26,6 +26,24 @@ The flagship example is Corbits Solution Builder (`corbitsdev/corbits-solution-b 6. **Deploy.** `deployWorkflow(transport, tenantId, { source: { kind: "asset", assetId, package: { format: "source", commitSha } }, entry, sourceOfferingIds, defaultSourceOfferingId })`. The chain freezes at deploy, so a model switch is a new deployment. A deploy needs a non-empty chain and a sidecar provisioner. 7. **Run.** `triggerWorkflowRun`, then fold `readWorkflowRunEvents` (or `createRunSession`, which polls every 2 s). Answer waits with `deliverWorkflowSignal(transport, tenantId, runId, { runId, signalName, signalId, payload })`. A repeated `signalId` with a different payload is a 409. Resolve tool approvals with `POST /approvals/:id/approve` or `/reject`, which needs `approval:*` / `resolve`. +## Sharing a hub between apps + +- Tenant slugs are one global namespace per hub, so prefix them: an app root tenant (``) and child tenants per project (`--`). Solution Builder finds its root by slug in `GET /api/me/principals` and creates it if absent. A slug clash returns 409, so re-resolve instead of failing. +- Catalog, providers, OAuth clients, credentials, and placement policy inherit down the tenant chain. Assets, roles, principals, and grants do not. Put shared keys in a parent tenant, and keep per-project assets, deployments, artifacts, and mail in a child. +- Do not put two apps in one tenant unless every asset and credential name is prefixed. Names are unique per tenant. +- Apps share the hub's provisioner pool, not a sidecar. Every deployment gets its own sidecar allocation. +- Every `ensure*` step adopts what exists and never overwrites. The tenant create is the only step that is not idempotent. + +## Chat is mail + +Chat is not a stock hub feature. The host mounts `@corbits/mailbox` (`use_skill corbits-hub-libs`). + +- A person's turn is a mail send to the run address (`@`). The mailbox `deliver` hook calls the hub's `POST /workflows/:runId/mail`. The first message fires the run, and later ones resume an `onTrigger` section. +- Replies arrive in the person's inbox over mailbox SSE (`/mailbox/me/inbox/events`). Threads follow `In-Reply-To` and `References`. +- Mail carries plain text only. Progress, tool calls, and approvals come from polled run events (`readWorkflowRunEvents`) and `/approvals`. +- `ChatThread` takes `messages` as a prop. You write the adapter from mail rows plus run events. +- Agent model context lives in the sidecar's agent repo and is not the transcript. + ## Rules that save time - Only the browser transport ships. Outside a browser, supply your own: base URL, cookie jar, JSON content type, `ApiError` mapping, empty 202 and 204 bodies. In process you can call `hub.app.fetch(new Request(origin + path))` with the session cookie. diff --git a/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md b/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md index a17768d26..e161f0fff 100644 --- a/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md @@ -6,7 +6,7 @@ user-invocable: false # Embedding an Interchange hub -Use this when your app owns the hub process: a desktop host, a single binary, or a server that mounts Interchange next to its own routes. For what the app then does over HTTP, see `use_skill interchange-client-apps`. +An embedded hub is a local control plane. `runLocal` has no control plane, and a remote hub has one elsewhere. Client code is identical for embedded and remote, so build against `Transport` and a base URL (`use_skill interchange-run-modes` for when to pick which). Use this when your app owns the hub process: a desktop host, a single binary, or a server that mounts Interchange next to its own routes. For what the app then does over HTTP, see `use_skill interchange-client-apps`. ## Stock vs composed diff --git a/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md b/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md index d7c0c6aea..7ba29e3bd 100644 --- a/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md @@ -1,29 +1,53 @@ --- name: interchange-run-modes -description: Pick where an Interchange agent runs (in-process, local hub, remote hub plus sidecar) and how sidecars, credentials, and placement work. +description: Pick where the Interchange control plane lives (none with runLocal, embedded hub, remote hub) and how sidecars, credentials, and placement work. user-invocable: false --- # Interchange run modes -Pick the smallest mode that meets the need. +The question is where the control plane lives. Your workflow and agent code is the same in every mode. -| Situation | Mode | -| ----------------------------------------------------- | -------------------------------------------------------------- | -| One agent inside your own CLI or app, one user | In-process `createAgent` with an isogit `contextDir` | -| Coding agent over a local tree | In-process plus `@intx/tools-posix` (and LSP), set `toolCwd` | -| Test a workflow | `runLocal` with scripted inference | -| Durable steps, crash resume, approvals, audit | Hub plus sidecar | -| Multiple users or tenants, grants, pushed credentials | Hub required | -| Host behind NAT, on a laptop, or firewalled | Sidecar dials out to the hub | -| "Desktop" personal agent | In-process, or a local hub that spawns a local sidecar process | -| Untrusted workload | Provisioner that declares an `isolation:*` capability | +| Tier | Control plane | Use it when | +| ----------------------------- | ------------------------------- | ----------------------------------------------------------------------------- | +| `runLocal` / in-process agent | none | Scripts, CLI tools, CI, tests, dev loops, one user, run ends with the process | +| Embedded hub | in your app process | You need hub features but want one local binary (desktop app) | +| Remote hub | a hosted hub, sidecars dial out | Shared team, runs must outlive any one machine, work placed on other hosts | -There is no dedicated desktop harness in the repo. Desktop means the local-process mode. Container, VM, Workers, and browser hosts are in progress, not shipped. +Embedded and remote differ only in a base URL. An app written against the hub `Transport` switches by config (`use_skill interchange-embed-hub`, `use_skill interchange-client-apps`). Moving up from `runLocal` is a deploy, not a rewrite, so start without a hub unless you need one of the reasons below. -## In-process (no hub) +## Skip the hub when -See `use_skill interchange-agents`. There is no tenancy, remote credential push, or multi-user auth. You supply `authorize` and audit yourself. +- Nothing to stand up: no database, keys, auth origin, or sidecar processes, and a cold start in milliseconds. +- State is inspectable and disposable (a directory), with no second source of truth. +- Tests and CI want hermetic runs with scripted inference and no ambient credentials. +- One person starts the run and watches it end. +- HITL is fine while the process lives: `run.signal(name, payload, signalId)` answers an `awaitSignal`. +- You want to avoid depending on the alpha hub surface. + +## Want a control plane when + +- Waits must outlive the process (approvals, `awaitSignal`, timers): the hub parks, persists, and resumes after a crash. `runLocal` has no durability and a crashed run is terminal. +- Credentials are shared and rotated: the hub seals provider keys and freezes a failover chain at deploy. +- Triggers run while you are away: mail, cron, webhooks. +- Several principals need grants, an audit trail, approvals routes, and spend accounting. +- Work should run in a sandbox or on another host (sidecar placement). +- Runs need addresses so people and agents can mail them. +- A deploy must be pinned to a commit. + +## runLocal + +`runLocal(definition, { authorize, hasUpstreamSignalResolver, ... })` returns `{ runId, complete, cancel, signal }`. `authorize` and `hasUpstreamSignalResolver` are required; pass `true` for a top-level run you will signal. + +- Supported: signals, authorize, actions (need `actionResolver`), loops (need `loopFns`), inline child workflows, timers, an in-memory event log in `RunResult.events`, cancel, and `resumeFromEvents` if you feed the log back yourself. +- Not supported: persistence, crash resume, drain, hub approval rows or `/approvals` routes, mail and triggers, tools, hub credentials, placement. +- The default step invoker is a stub that returns `{ output: null }`. To run a model, pass an `invokeStep` that builds `createAgent` and calls `agent.send`. +- An untimed `awaitSignal` inside a `childWorkflow` fails the child, because nothing can answer it. +- Unverified: whether `onTrigger` sections run locally. + +For a single agent without a workflow, use in-process `createAgent` with a git-backed store and `@intx/tools-posix` (`use_skill interchange-agents`). You supply `authorize` and audit. + +Desktop means a local process: in-process, or an embedded hub that spawns a local sidecar process. There is no dedicated desktop harness. Container, VM, Workers, and browser hosts are in progress, not shipped. ## Hub plus sidecar diff --git a/plugins/corbits-skills/skills/interchange-workflows/SKILL.md b/plugins/corbits-skills/skills/interchange-workflows/SKILL.md index cd631b892..e8f44c455 100644 --- a/plugins/corbits-skills/skills/interchange-workflows/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-workflows/SKILL.md @@ -85,6 +85,23 @@ const result = await run.complete; // { runId, terminalStatus, outputs } `POST /api/tenants/:t/workflows/deployments` installs, probes (on an airlocked sidecar), gates on the operator's approval set, freezes, then provisions a sidecar. Trigger with `/:runId/mail`, answer waits with `/:runId/signals`. The in-tree hub registers no provisioner, so it cannot deploy until one is injected: `use_skill interchange-run-modes`. +## Handoff between workflows + +There is no primitive that starts or signals another deployment from inside a workflow. Choose by need: + +| Need | Use | +| ---------------------------------------- | ------------------------------------------------------------ | +| Human turn into a run | Mail to the run address | +| Human decision into a parked run | `deliverWorkflowSignal` with a stable `signalId` | +| Agent to agent, conversational | `mail_send` to the peer's run address (unverified: triggers) | +| Bounded sub-task, no human | `childWorkflow` (an untimed park inside fails the child) | +| Rework loop with a human gate in one run | `loop` with `awaitSignal` in the body | +| Many human turns in one living run | `onTrigger` section | +| Large or durable content | Artifact or memory reference plus sha256, not the payload | +| Start a different deployment | The client or host (ensure deploy, then trigger) | + +Solution Builder orchestrates from the client: it mails per-stage specialists, sends `project.decision` signals to one project workflow, and passes artifact references. On redeploy it starts a new generation and replays recorded decisions through it. + ## Need more - Full pitfall list: upstream `docs/WORKFLOW_AUTHORING.md` and `examples/workflow-quickstart`. From 16d7558f815e0218f20e73c9d6e23c161d709f32 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 1 Oct 2026 15:53:37 -0700 Subject: [PATCH 4/4] docs(skills): split interchange hub skills by task and add runnable examples --- docs/PRODUCT.md | 2 +- .../skills/interchange-chat/SKILL.md | 47 ++++++++++ .../skills/interchange-client-apps/SKILL.md | 70 +++++--------- .../skills/interchange-embed-hub/SKILL.md | 63 +++++++------ .../skills/interchange-hub-api/SKILL.md | 51 ++++++++++ .../skills/interchange-hub-setup/SKILL.md | 89 ++++++++++++++++++ .../skills/interchange-run-modes/SKILL.md | 94 ++++++++++--------- .../skills/interchange-workflows/SKILL.md | 22 ++--- .../skills/interchange/SKILL.md | 2 +- src/telemetry/classify.ts | 3 + 10 files changed, 311 insertions(+), 132 deletions(-) create mode 100644 plugins/corbits-skills/skills/interchange-chat/SKILL.md create mode 100644 plugins/corbits-skills/skills/interchange-hub-api/SKILL.md create mode 100644 plugins/corbits-skills/skills/interchange-hub-setup/SKILL.md diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 62994f991..92d8e7749 100644 --- a/docs/PRODUCT.md +++ b/docs/PRODUCT.md @@ -107,7 +107,7 @@ the file path and parse details. The TUI has an extensible slash-command framework. Built-ins include `/help` (shortcut + command overlay), `/model` (models-only picker for connected accounts; **Alt+A** or `/connect` adds a provider), `/settings`, `/permissions`, `/plugins`, `/clear`, `/new`, `/compact` (fold conversation context now, optional trailing instructions to the summarizer; does not wait for the 60% occupancy governor; idle success shows the fold and does not start a new turn), `/mcp` (enable, disable, or remove servers), `/handoff [optional instructions]` (folds context through the shared operator pipeline, then immediately starts the next turn with the instructions as the inbound content — default copy when omitted; unlike `/compact`, which stops after the fold, handoff always re-infers, so the operator can pivot goals without `/clear`; a handoff issued mid-tool-batch queues behind the in-flight batch and whichever boundary fires first runs the single fold), and `/yolo` (`/yolo [on|off|toggle]`, bare `/yolo` toggles), plus a `/` command per available workflow. `/yolo` persists skip-permissions to the active settings file. That file is the user-global `~/.corbits/settings.json` by default, making the setting machine-wide; explicit `--config ` selects a different active file, and `/yolo` writes that file. An ordinary TUI launch without the same `--config` returns to the user-global source and does not modify the custom file. `--dangerously-skip-permissions` and `--yolo` are process-only aliases. Secret-guard and authz still apply. When a session starts with its active persisted setting already on, the TUI and `corbits exec` warn that permission prompts are disabled by saved settings at the active settings path and direct the operator to edit that file to re-enable them. Plugins can register additional commands. -**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. Background reference skills (`typescript`, `git-worktrees`, and the `interchange` hub with its `interchange-agents`, `interchange-workflows`, `interchange-run-modes`, `interchange-embed-hub`, and `interchange-client-apps` references) have no slash; the model loads them with `skill_search` and `use_skill`, and each one ends with "need more, go here" pointers to the next skill in the chain. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request` finds or opens the PR for the current branch, and `/review` takes a PR target and reviews it from a worktree; `/docs` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` takes a plan and a ticket to a pushed branch (worktree, per-commit planner/build/reviewer loop, whole-branch review, push, hand off to `/pull-request`) — it does not steal planning from `/plan`. Substantial coder work consumes a planner / `/plan` plan first; tiny parent-DIY stays plan-optional. `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/issue` finds or creates the tracker issue: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). There is no first-party dispatch skill: the dispatch director orchestrates natively. `typescript` stays `use_skill` only (`user-invocable: false`); `git-worktrees` is a background library that is not listed for `use_skill`. Designer and the other specialists are not slashes; they remain closed directors via `spawn_agent(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `skill_search` (descriptions) then `use_skill` (body). Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone. +**Default skills** exist out of the gate as first-party slash **actions**, not director names: `/implement`, `/plan`, `/refactor`, `/review`, `/pull-request`, `/issue`, `/docs`, `/interview`. Each one is a how-to playbook — the slash sends the skill body to the primary, which follows the steps. Skills do not assign identity or route the fleet; that stays on director system prompts. Background reference skills (`typescript`, `git-worktrees`, and the `interchange` hub with its `interchange-agents`, `interchange-workflows`, `interchange-run-modes`, `interchange-embed-hub`, `interchange-hub-setup`, `interchange-hub-api`, `interchange-chat`, and `interchange-client-apps` references) have no slash; the model loads them with `skill_search` and `use_skill`, and each one ends with "need more, go here" pointers to the next skill in the chain. `/review` classifies the target first, then dispatches a selected fleet; `/pull-request` finds or opens the PR for the current branch, and `/review` takes a PR target and reviews it from a worktree; `/docs` is how to maintain PRODUCT / ARCHITECTURE / IMPLEMENTATION; `/implement` takes a plan and a ticket to a pushed branch (worktree, per-commit planner/build/reviewer loop, whole-branch review, push, hand off to `/pull-request`) — it does not steal planning from `/plan`. Substantial coder work consumes a planner / `/plan` plan first; tiny parent-DIY stays plan-optional. `/plan` authors an eng change plan (files, AC, non-goals, risks, ordered steps) and does not implement. `/issue` finds or creates the tracker issue: Linear MCP when available; otherwise it `ask_operator`s for the platform (GitHub etc.) and persists `Preferred issue tracker` in `.corbits/MEMORY.md` (GitHub via `gh issue create`). There is no first-party dispatch skill: the dispatch director orchestrates natively. `typescript` stays `use_skill` only (`user-invocable: false`); `git-worktrees` is a background library that is not listed for `use_skill`. Designer and the other specialists are not slashes; they remain closed directors via `spawn_agent(agent=…)`. There is no catch-all worker. Slash names are also available to the model via `skill_search` (descriptions) then `use_skill` (body). Disable the catalog in `/plugins` (`corbits-skills`) if you want them gone. Providers are **models-first**: there is no standalone `/login` command. `/model` opens a **models-only list** (Recent, Favorites, then connected provider/model rows) — type-to-filter owns printable keys, so Connect is never a bare letter. **Alt+A** or `/connect` opens a dedicated add-provider selector over every first-class kind (OpenAI dual-path ChatGPT OAuth or API key, xAI, OpenCode Zen, Anthropic, Google, OpenCode Go, Z.AI Coding Plan, Ollama, Custom), each annotated with its live account count and never filtered out for “already connected.” **Alt+F** toggles favorite on the highlighted model. **Alt+D** persists the highlighted pair as the default without switching the live session. Advanced provider drill-down (edit/delete/tiers) stays on the advanced surface, not a bare printable key while the model list is filtering. OAuth providers open their existing browser login with a named account step so multiple accounts per kind coexist (`codex/work`, …). API-key providers use the same named-instance step before the key (auth-only form: instance name + key + fixed catalog base URL), so personal and team keys land as distinct catalog rows (`openai/default`, `anthropic/work`, …); reusing a name re-keys that instance after confirm. Custom remains a free-form single endpoint (full manual form). Successful connect refreshes the catalog and reopens the model list focused on the new account’s default model. OpenCode Go lists models from the live `/zen/go/v1/models` catalog (packaged seed on fetch failure), routes each by its protocol metadata (chat completions, OpenAI responses, or Anthropic messages) and can show subscription usage in the status bar when active (rolling 5h / weekly / monthly windows when the usage API responds; omitted on auth or network failure). When Go returns a quota or rate-limit error — including some HTTP 400 responses that carry limit payloads — Corbits classifies them so quota aborts cleanly and short provider rate limits remain retryable. On a free-tier or subscription quota hit, wait for the window to reset or use OpenCode Zen free models. diff --git a/plugins/corbits-skills/skills/interchange-chat/SKILL.md b/plugins/corbits-skills/skills/interchange-chat/SKILL.md new file mode 100644 index 000000000..3be660361 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-chat/SKILL.md @@ -0,0 +1,47 @@ +--- +name: interchange-chat +description: Build chat, run progress, signals, and approvals for an Interchange workflow in a UI. Load when a person talks to a deployed agent or workflow, or when showing run status. +user-invocable: false +--- + +# Chat and run observation + +Chat on Interchange is mail. People and agents share one mail bus, and a deployed run has an address. Progress is a separate feed. + +## Pick the feed + +| Need | Source | +| -------------------------------- | -------------------------------------------------------------------------------------- | +| Person sends a message | Mail to `@` (`triggerWorkflowRun` or the mailbox `send`) | +| Agent reply text | The person's inbox: mailbox routes and SSE (`@corbits/mailbox`) | +| Status, steps, parked waits | Poll run events (`readWorkflowRunEvents`, or `createRunSession` which polls every 2 s) | +| Tool calls, reasoning | Run events or `GET T/workflows/runs/:runId/turns`, never mail (mail is plain text) | +| Human decision for a waiting run | `deliverWorkflowSignal` with a stable `signalId` | +| Tool approval | `GET T/approvals`, then `POST T/approvals/:id/approve` or `/reject` | + +## How a turn flows + +1. The first mail to a run address fires the run. The hub treats inbound mail to a run address as the trigger, whether it came from the HTTP route or an agent's `mail_send`. +2. While the run lives, later mail resumes its `onTrigger` section as input. A terminal run cannot be re-fired (409). +3. Replies land in the sender's inbox and publish on the mailbox event bus. Threads follow `In-Reply-To` and `References`. + +## What the host must provide + +The stock hub has no mailbox routes. The host mounts `@corbits/mailbox` at `T/mailbox`, wraps the hub's `persistMail` so agent mail reaches inboxes, and supplies a `deliver` that sends run-addressed mail through `POST T/workflows/:runId/mail` with the caller's session. See `use_skill corbits-hub-libs`. Workbench `apps/hub/src/mailbox-send.ts` is the reference. The default event bus is in memory, so use a shared one for multiple replicas. + +## UI + +`@corbits/react-ui` `ChatThread` takes `messages` as a prop: `{ id, role: "user" | "agent" | "system", parts, createdAt }` where parts are text, reasoning, tool, or file. Write the adapter that maps inbox rows to text parts and run events to tool and reasoning parts. A person's address is the auth user id at the tenant domain, lowercased. + +## Cautions + +- `onTrigger` body runs can silently skip when a second top-level run of the same deployment reaches the section (upstream known limitation, INTR-552). Do not build one run per conversation on `onTrigger` without testing a second run. +- An untimed park (`awaitSignal`, or an `approval: "ask"` tool) inside a `childWorkflow` is refused. Put human waits in the top-level run or in a `loop` body. +- Agent model context lives in the sidecar's agent repo. It is not the transcript, and compaction does not touch the mail. + +## Need more + +- Setting up the hub and deploying: `use_skill interchange-hub-setup`. +- Mounting mailbox, artifacts, memory: `use_skill corbits-hub-libs`. +- UI components: `use_skill corbits-ui-apps`. +- Handoff between workflows: `use_skill interchange-workflows`. diff --git a/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md b/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md index baeacf21c..a1c8882e1 100644 --- a/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md @@ -1,62 +1,40 @@ --- name: interchange-client-apps -description: Build a client-driven app that sets up its own Interchange hub (tenants, auth, grants, providers, installs, deploys, runs) using stock @intx and @corbits packages. +description: Decide how an app builds on an Interchange hub it drives itself, and which skill to load next. Load when building a client-driven app, a desktop agent product, or a UI over a hub. user-invocable: false --- -# Client-driven apps on Interchange +# Client-driven apps -A client-driven app drives the whole setup. The hub boots vanilla (migrate, mount, serve). The app signs in, creates its tenants, grants, providers, and assets, deploys its workflows, and runs them, all over the hub's HTTP API using stock `@intx/*` and `@corbits/*` packages. Nothing about the product is baked into the hub. +A client-driven app owns its own setup: it creates its tenants, auth, grants, providers, workflow assets, and deploys over the hub's HTTP API, using stock `@intx/*` and `@corbits/*` packages. The hub stays vanilla. Reference app: Corbits Solution Builder (`corbitsdev/corbits-solution-builder`, `packages/installer`), with workbench (`corbitsdev/workbench`) as the server-composition reference. -The flagship example is Corbits Solution Builder (`corbitsdev/corbits-solution-builder`). Its architecture note says it directly: the client runs the installer over the hub on first launch and after every credential change. Read `packages/installer/src/*.ts` first. Workbench (`corbitsdev/workbench`) is the server-composition reference. +## Find your task -## The shape +| You are... | Load | +| ---------------------------------------------------- | ----------------------- | +| Deciding whether you need a hub at all | `interchange-run-modes` | +| Starting the hub inside your app, or pointing at one | `interchange-embed-hub` | +| Writing the app's first-run install and deploys | `interchange-hub-setup` | +| Calling a route and need its body | `interchange-hub-api` | +| Building chat, run status, signals, approvals | `interchange-chat` | +| Writing the workflow the app deploys | `interchange-workflows` | +| Mounting mailbox, artifacts, memory, cron | `corbits-hub-libs` | +| Building the UI | `corbits-ui-apps` | -- **Host** (once per launch): picks embedded or remote hub, opens the database, migrates, mints keys, mounts the hub, starts sidecar reconcilers. See `use_skill interchange-embed-hub`. -- **Client** (browser or script): everything else, through one `Transport` (`fetch(method, path, body)` plus `subscribe`). -- Keep an `install()` that is idempotent (list-then-create, tolerate 409). Run it on first launch and after changes. Solution Builder keeps it free of any database import. +## Shape -## Bootstrap recipe (client side) - -1. **Auth.** better-auth, email and password. `POST /api/auth/sign-up/email {name,email,password}`, on 422 `POST /api/auth/sign-in/email`. Replay the `better-auth.session_token` cookie. The REST API has no bearer or service token, so an app that acts as "system" holds a user account and keeps its cookie. A desktop app can mint one fixed local owner with a keychain-held password. -2. **Tenant.** `POST /api/tenants {name, slug, parentId?}`. The caller becomes the owner principal with `*`/`*` allow. Find existing ones via `GET /api/me` and `GET /api/me/principals`. A child tenant (`parentId`) per project inherits the parent's catalog, assets, and placement policy. Do not treat the hierarchy as a trust boundary yet: the create route does not appear to check the caller's rights on `parentId` (unverified). -3. **Roles and grants.** `POST /roles`, `POST /grants {roleId|principalId, resource, action, effect, origin}`, `POST /principals/:p/roles/:r`. Model app authorities as grants such as `authority:` / `hold`. An invited member must be PATCHed to `active` before the tenant accepts them. -4. **Inference.** In order: `POST /providers` (set `apiBaseUrl`), `POST /credentials`, `POST /catalog/models`, `POST /catalog/providers` (bound to the credential), `POST /catalog/offerings`. A model provider's credential cannot be repointed, so rotate the same credential id. The offering ids are the failover chain you give each deploy. -5. **Install code.** `POST /assets {kind:"workflow"|"skill"|"package-registry", name}`. Push source through a git token (`POST /git-tokens`, `can_read` and `can_push`, short expiry, revoke after). Poll until the push is visible before deploying. -6. **Deploy.** `deployWorkflow(transport, tenantId, { source: { kind: "asset", assetId, package: { format: "source", commitSha } }, entry, sourceOfferingIds, defaultSourceOfferingId })`. The chain freezes at deploy, so a model switch is a new deployment. A deploy needs a non-empty chain and a sidecar provisioner. -7. **Run.** `triggerWorkflowRun`, then fold `readWorkflowRunEvents` (or `createRunSession`, which polls every 2 s). Answer waits with `deliverWorkflowSignal(transport, tenantId, runId, { runId, signalName, signalId, payload })`. A repeated `signalId` with a different payload is a 409. Resolve tool approvals with `POST /approvals/:id/approve` or `/reject`, which needs `approval:*` / `resolve`. - -## Sharing a hub between apps - -- Tenant slugs are one global namespace per hub, so prefix them: an app root tenant (``) and child tenants per project (`--`). Solution Builder finds its root by slug in `GET /api/me/principals` and creates it if absent. A slug clash returns 409, so re-resolve instead of failing. -- Catalog, providers, OAuth clients, credentials, and placement policy inherit down the tenant chain. Assets, roles, principals, and grants do not. Put shared keys in a parent tenant, and keep per-project assets, deployments, artifacts, and mail in a child. -- Do not put two apps in one tenant unless every asset and credential name is prefixed. Names are unique per tenant. -- Apps share the hub's provisioner pool, not a sidecar. Every deployment gets its own sidecar allocation. -- Every `ensure*` step adopts what exists and never overwrites. The tenant create is the only step that is not idempotent. - -## Chat is mail - -Chat is not a stock hub feature. The host mounts `@corbits/mailbox` (`use_skill corbits-hub-libs`). - -- A person's turn is a mail send to the run address (`@`). The mailbox `deliver` hook calls the hub's `POST /workflows/:runId/mail`. The first message fires the run, and later ones resume an `onTrigger` section. -- Replies arrive in the person's inbox over mailbox SSE (`/mailbox/me/inbox/events`). Threads follow `In-Reply-To` and `References`. -- Mail carries plain text only. Progress, tool calls, and approvals come from polled run events (`readWorkflowRunEvents`) and `/approvals`. -- `ChatThread` takes `messages` as a prop. You write the adapter from mail rows plus run events. -- Agent model context lives in the sidecar's agent repo and is not the transcript. +- **Host**, once per launch: embedded or remote hub, migrations, keys, mount, sidecar provisioner. +- **Client**, browser or script: everything else through one `Transport`. +- **`install()`**: idempotent, runs on first launch and after changes, and imports no database code. +- **Config**: one hub-URL setting switches embedded to remote with no client change. ## Rules that save time -- Only the browser transport ships. Outside a browser, supply your own: base URL, cookie jar, JSON content type, `ApiError` mapping, empty 202 and 204 bodies. In process you can call `hub.app.fetch(new Request(origin + path))` with the session cookie. -- `@intx/hub-client` covers workflows only. Solution Builder's `installer/src/hub.ts` is the broader typed client for tenants, roles, grants, and the catalog. Validate every response with arktype. -- List endpoints return inherited rows. Filter assets by `tenantId` or you will adopt the parent's same-named asset. Send signals to the tenant the deployment belongs to. -- Deploy lazily, per project or stage, when the work first needs it. Do not provision everything at install. -- Client-side checks (for example guarding which grants the app may mint) are not security. The hub authorizes. -- Never put secrets in responses, logs, or prompts. Credentials are sealed by the hub's cipher. +- Start without a hub if you only need to run a workflow in one process (`runLocal`). Move up when waits must outlive the process, credentials are shared, or triggers run while you are away. +- Deploy per project or stage, when the work first needs it. Do not provision everything at install. +- A person's chat turn is mail to the run address, not a hub chat API. +- The REST API has no service token. An app acting as "system" holds a user account and keeps its cookie. ## Need more -- Composing, embedding, and running the hub itself: `use_skill interchange-embed-hub`. -- Sidecars, placement, and credential delivery: `use_skill interchange-run-modes`. -- Workflow code you will deploy: `use_skill interchange-workflows`. -- Corbits libraries to mount (mailbox, artifacts, OAuth login, cron, memory): `use_skill corbits-hub-libs`. UI and reference apps: `use_skill corbits-ui-apps`. -- Overview: `use_skill interchange`. +- Overview: `use_skill interchange`. Package catalog: `use_skill corbits`. diff --git a/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md b/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md index e161f0fff..a3384f312 100644 --- a/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md @@ -1,51 +1,56 @@ --- name: interchange-embed-hub -description: Compose, embed, and run an Interchange hub inside your own app host (database, keys, auth origin, sidecar provisioner, reconcilers, vendoring). +description: Run an Interchange hub inside your own app process (desktop app, single binary, server) with its database, keys, auth origin, and sidecar provisioner. Load when an app must host its own hub. user-invocable: false --- -# Embedding an Interchange hub +# Hosting an Interchange hub -An embedded hub is a local control plane. `runLocal` has no control plane, and a remote hub has one elsewhere. Client code is identical for embedded and remote, so build against `Transport` and a base URL (`use_skill interchange-run-modes` for when to pick which). Use this when your app owns the hub process: a desktop host, a single binary, or a server that mounts Interchange next to its own routes. For what the app then does over HTTP, see `use_skill interchange-client-apps`. +An embedded hub is a local control plane. Client code is the same as for a remote hub, only the base URL differs, so build the app against `Transport` (`use_skill interchange-hub-setup`). Pick the lowest row that fits: -## Stock vs composed +| Situation | Do this | +| -------------------------------------------------- | -------------------------------------------------------------------------------- | +| A hub already runs somewhere | Embed nothing. Set the hub URL. Run no migrations and place no sidecars locally. | +| Local, Postgres, env-driven | `createHubServer({ sidecarProvisioners, probeSidecarProvisioners })` | +| Local, your own database (pglite) or keychain keys | Copy the hub composition and inject your pieces | -- Upstream `createHubServer` (`apps/hub/src/server.ts`) takes only provisioner and placement options (`sidecarProvisioners`, `probeSidecarProvisioners`, choosers, capability rules). The database, keys, and data dir come from env: `DB_*`, `HUB_DATA_DIR`, `CREDENTIAL_ENCRYPTION_KEY`, `PRINCIPAL_KEY_ENCRYPTION_KEY`, `BETTER_AUTH_BASE_URL`, `BETTER_AUTH_SECRET`. -- An app that needs its own database, auth, or key store copies that composition. Workbench (`apps/hub/src/server.ts`, about 860 lines, real Postgres) and Solution Builder (`packages/embed-hub`, pglite) both do. Expect to re-port when upstream changes. -- The reusable surface is `createApp` and `mountHubRoutes` from `@intx/hub-api` (pass your own `getSession`, which is not tied to better-auth), plus the factories in `@intx/hub-sessions` and `@intx/db`. Mount extra Hono routes (`@corbits/*` libraries) with `createRequireGrant`. +A local app should switch on one setting. Unset means embedded, set means remote. Never migrate against a remote hub. -## The composition, in order +## Stock hub -`createDB` then `createAuth(db)`, credential cipher and principal key store (two distinct 32-byte hex keys), agent repo store (signs with the hub key), asset service, sidecar credential resolver and router, event collectors, session service, sidecar plugin registries (deployment and probe provisioners), workflow allocation and dispatch services, the allocation reconciler, four reconciliation schedulers, then `createApp`. Serve with `Bun.serve({ fetch, websocket })` (the sidecar WebSocket lives at `/api/sidecars/ws`). +`createHubServer` is in `@intx/hub-app/server`, which is private and exported only under the `intx-src` condition. Use it from the upstream workspace or a vendored copy. It reads `DB_HOST DB_PORT DB_USER DB_PASSWORD DB_NAME`, `HUB_DATA_DIR`, `CREDENTIAL_ENCRYPTION_KEY`, `PRINCIPAL_KEY_ENCRYPTION_KEY`, `BETTER_AUTH_BASE_URL`, `BETTER_AUTH_SECRET`, and `PORT`. It registers no sidecar provisioner, so you must pass one or deploys fail. Upstream's reference is `tests/admin-ui-e2e/harness/`: -Start the schedulers or allocations never progress. Omit the allocation service and deploys fail with `workflow_provisioning_unavailable`. Omit the cipher and secrets are stored unencrypted with only a warning. +```ts +const lp = createLocalProcessSidecarProvisioner({ + dataRoot: `${HUB_DATA_DIR}/local-sidecars`, +}); +export default await createHubServer({ + sidecarProvisioners: [lp.provisioner], + probeSidecarProvisioners: [lp.provisioner], +}); +process.once("SIGTERM", () => void lp.shutdown().then(() => process.exit(0))); +``` -## Embedded vs remote +Migrate first with `drizzle-kit migrate` in `packages/db`. `runMigrations(cfg, { schema })` has no ledger and is not idempotent, so guard it (check the `user` table exists) and serialize boots with an advisory lock. -Solution Builder switches on one env var (`_HUB_URL`). Unset, the hub is a Hono app inside the host process and calls go to `hub.app.fetch` with no socket. Set, the host mounts nothing, runs no migrations, and places no sidecars, because a local migration would create a second, divergent control plane. +## Own composition -## Database and migrations +Copy `apps/hub/src/server.ts`, as workbench (Postgres) and Solution Builder `packages/embed-hub` (pglite) do. Order: `createDB`, `createAuth`, credential cipher and principal key store, agent repo store, asset service, sidecar credential resolver and router, session service, provisioner registries, allocation service and reconciler, four schedulers, `createApp`, then `Bun.serve({ fetch, websocket })`. The sidecar socket is `/api/sidecars/ws`. Mount extra Hono routes (`@corbits/*`) behind `createRequireGrant`. -- Upstream targets Postgres. `runMigrations(cfg, { schema })` has no ledger and is not idempotent, so guard it (check a table exists) and serialize boots with an advisory lock. The standard flow is `drizzle-kit migrate`. Run each Corbits library's migrations after it. -- pglite is not supported upstream. Solution Builder runs it with one vendor patch (`createDB({ handle, close })`), a schema-bound `drizzle(pglite, { schema })`, a shim that returns rows in postgres.js shape, and its own migration ledger. pglite is single-writer: claim the data dir with a pid file, and never auto-migrate a database stamped by an older revision. +Skip a piece and it fails quietly: no schedulers means allocations never progress, no allocation service means `workflow_provisioning_unavailable`, no cipher means plaintext secrets with only a warning. -## Keys and auth origin +Expect to re-port when upstream changes. If you need pglite, you also carry the `createDB({ handle, close })` patch, a schema-bound drizzle handle, a shim to postgres.js row shape, and your own migration ledger. pglite is single-writer, so claim the data dir with a pid file. -- Mint the credential key, principal key, and hub signing key once and persist them (OS keychain or an owner-only file). Never mint when the store merely failed to answer. Persist the signing key so earlier signed commits still verify. -- Set `BETTER_AUTH_BASE_URL` to the exact origin clients use. The default is `http://localhost:3000`, which makes better-auth reject a loopback sign-up as CSRF. -- Loopback is not authentication. Solution Builder adds a host session token (keychain, exchanged for an HttpOnly cookie) in front of `/api/*`, and lets the sidecar WebSocket bypass it because the hub checks the sidecar's own token. +## Rules that bite -## Sidecars - -Sidecars exist only through a `SidecarProvisioner`. Spawn the sidecar entry as a child process with an allowlisted env: `HUB_WS_URL`, `SIDECAR_ID`, `SIDECAR_TOKEN`, `SIDECAR_DATA_DIR`, `SIDECAR_CREDENTIAL_ENCRYPTION_KEY`, optional `SIDECAR_ADAPTER_MANIFEST` (JSON array of `{ provider, specifier, export }`, trusted input that runs code). Write a pid file before anything else, fence by generation, and reap children on shutdown, because sidecars outlive a killed host and dial a hub that is gone. The WebSocket URL is part of the binding fingerprint, so keep the port stable. - -## Consuming Interchange - -Prefer published `@intx/*` packages. Solution Builder vendors the whole upstream workspace (pinned revision, a `PATCHES.md` with a reason and kill date per local edit) because it needs pglite injection, a newer `@intx/workflow`, and an offline compiled binary. Published tarballs carry an `intx-src` export condition that points at source they do not ship, so sidecars and deployed workflows need built `dist/`. Resolve exactly one copy of each `@intx/*` package. +- Mint the credential key, principal key, and signing key once and persist them (OS keychain or an owner-only file). Persist the signing key so old signed commits verify. Never mint a key because the store failed to answer. +- `BETTER_AUTH_BASE_URL` must equal the origin clients use. The default is `http://localhost:3000`, so any other origin fails sign-up as CSRF. +- Loopback is not authentication. Put a host session in front of `/api/*`, and let only the sidecar socket bypass it. +- A sidecar is a child process started by your `SidecarProvisioner.ensure`. Pass only `HUB_WS_URL SIDECAR_ID SIDECAR_TOKEN SIDECAR_DATA_DIR SIDECAR_CREDENTIAL_ENCRYPTION_KEY`, plus optional `SIDECAR_ADAPTER_MANIFEST` (trusted JSON, it runs code). Write a pid file, fence by generation, and reap children on shutdown. Keep the hub port stable, because the socket URL is part of the allocation fingerprint. +- Published `@intx/*` packages resolve to `dist`. The `intx-src` condition needs source they do not ship. Resolve exactly one copy of each package. Solution Builder vendors the whole upstream workspace (pinned revision, `PATCHES.md` with a reason per local edit) for pglite, a newer workflow package, and an offline binary. ## Need more -- What the app does after the hub is up: `use_skill interchange-client-apps`. -- Sidecar protocol, placement, credentials: `use_skill interchange-run-modes`. +- App-side setup and deploys: `use_skill interchange-hub-setup`. +- Sidecars, placement, credentials: `use_skill interchange-run-modes`. - Libraries to mount: `use_skill corbits-hub-libs`. -- Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange-hub-api/SKILL.md b/plugins/corbits-skills/skills/interchange-hub-api/SKILL.md new file mode 100644 index 000000000..6bad1cb2c --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-hub-api/SKILL.md @@ -0,0 +1,51 @@ +--- +name: interchange-hub-api +description: Interchange hub REST routes and request bodies (auth, tenants, roles, grants, providers, catalog, assets, deploys, runs, approvals). Load when writing code that calls a hub over HTTP. +user-invocable: false +--- + +# Interchange hub REST API + +Live spec: `GET /openapi.json` on any running hub. Generated reference: `docs/API.md` upstream. Read those before trusting this table. Auth is a better-auth session cookie only (no bearer or API key), except git, which takes a git token. + +Below, `T` = `/api/tenants/:tenantId`. Lists paginate with `?limit=&cursor=` and return `{ data, nextCursor }`. + +| Step | Call | +| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | +| Sign up / in | `POST /api/auth/sign-up/email {name,email,password}` (422 if exists), `POST /api/auth/sign-in/email {email,password}` | +| My tenants | `GET /api/me/principals` gives `{principalId, tenantId, tenantSlug, kind, status}`; `GET /api/me` | +| Create tenant | `POST /api/tenants {name, slug, parentId?}` (409 on slug clash, caller becomes owner with `*`/`*`) | +| Roles | `POST T/roles {name}`, `POST T/principals/:p/roles/:r` (409 if held) | +| Grants | `POST T/grants {roleId or principalId, resource, action, effect: allow/ask/deny, origin: role/creator/invoker/system}` | +| Invite | `POST T/members/invite {email, roleId?}` then `PATCH T/principals/:id {status:"active"}` | +| Provider | `POST T/providers {name, plugin, apiBaseUrl}` | +| Credential | `POST T/credentials {name, providerId, secret, type:"api_key"}` | +| Model | `POST T/catalog/models {canonicalName}` | +| Model provider | `POST T/catalog/providers {name, plugin, baseURL, credentialId}` | +| Offering | `POST T/catalog/offerings {modelId, providerId, priority?, capabilities?, quirks?}` | +| Asset | `POST T/assets {kind: workflow/skill/package-registry/agent-state, name}`, `GET T/assets?kind=&inherited=` | +| Git token | `POST T/git-tokens {name, resource, refPattern, actions:["can_read","can_push"], expiresAt}` gives `secret` once | +| Push code | git over HTTP at `T/assets/:kind/:name.git`, token as the basic-auth password, push `HEAD:main` | +| Deploy | `POST T/workflows/deployments {source:{kind:"asset",assetId,package:{format:"source",commitSha}}, entry, sourceOfferingIds, defaultSourceOfferingId}` | +| Deploy status | `GET T/workflows/deployments` until `deployed` (failure states: `failed`, `destroy_failed`) | +| Trigger / chat | `POST T/workflows/:deploymentId/mail {content, attachments?}` gives `{runId, address, messageId}` | +| Signal | `POST T/workflows/:deploymentId/signals {runId, signalName, signalId, payload?}` | +| Run events | `GET T/workflows/:deploymentId/runs`, then `.../runs/:eventRunId/events`, or `GET T/workflows/runs/:runId/events` | +| Approvals | `GET T/approvals`, `POST T/approvals/:id/approve` or `/reject` (grant `approval:`/`resolve`) | + +## Facts that cause bugs + +- Deploy supports only `source.kind: "asset"` and needs a sidecar provisioner. Without one you get 409 `workflow_provisioning_unavailable`. +- The deploy freezes the offering chain. A different model means a new deployment. +- A provider needs `apiBaseUrl` or source-ref deploys cannot seal the credential. A model provider's credential cannot be repointed after creation. +- Resource and action pairs seen in routes: `asset:*` create/read, `workflow:*` create/read, `workflow-run:` read/manage, `credential:*`/`provider:*`/`role:*`/`grant:*`/`principal:*` create/read/manage, `git-token:*` create. +- `@intx/*` tool packages resolve through a `workspace-builtins` package-registry asset. Publish it or tool closures fail. Upstream `bin/dev --seed` does this. +- `PATCH T` (tenant) checks membership only, despite docs saying admin. +- `POST /api/tenants` does not check your right to the `parentId` you name. Do not treat the tenant tree as a security boundary. +- Solution Builder's vendored hub adds `POST T/assets/:id/tree` and `GET T/assets/:id/blob`. They are not upstream, so use git when talking to a stock hub. + +## Need more + +- Calling these in order from an app: `use_skill interchange-hub-setup`. +- Chat and run observation: `use_skill interchange-chat`. +- Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange-hub-setup/SKILL.md b/plugins/corbits-skills/skills/interchange-hub-setup/SKILL.md new file mode 100644 index 000000000..9a16a8d4d --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-hub-setup/SKILL.md @@ -0,0 +1,89 @@ +--- +name: interchange-hub-setup +description: Write the app-side install() that signs in, creates tenants, grants, providers, assets, and deploys workflows on an Interchange hub. Load when an app must set up its own hub. +user-invocable: false +--- + +# App-driven hub setup + +The app, not an operator, sets the hub up. It talks to a stock hub over HTTP, the same code for an embedded or a remote hub (`use_skill interchange-run-modes`). Write one idempotent `install()`, run it at first launch and after any credential change. + +## Before you start + +- `@intx/hub-client` and `@intx/hub-app` are private, not on npm. Copy the small `Transport` below, or vendor the upstream workspace as Solution Builder does. `@intx/hub-api`, `hub-sessions`, `db`, and `workflow` are published. +- Check `GET /openapi.json` on a running hub for current bodies (`use_skill interchange-hub-api`). + +## The transport + +`createBrowserTransport` has no base URL or cookie jar, so outside a browser write your own. It must send JSON, replay cookies, turn errors into an `ApiError(status, code, message)`, and return `undefined` for an empty 202 or 204. + +```ts +const jar = new Map(); +export const transport: Transport = { + async fetch(method, path, body) { + const res = await fetch(BASE + path, { + method, + headers: { + ...(body !== undefined && { "content-type": "application/json" }), + cookie: [...jar].map(([k, v]) => `${k}=${v}`).join("; "), + }, + body: body === undefined ? undefined : JSON.stringify(body), + }); + for (const c of res.headers.getSetCookie()) { + const [kv] = c.split(";"); + const i = kv.indexOf("="); + jar.set(kv.slice(0, i), kv.slice(i + 1)); + } + if (!res.ok) throw await toApiError(res); + const text = await res.text(); + return text ? JSON.parse(text) : undefined; + }, + subscribe() { + throw new Error("poll run events instead"); + }, +}; +``` + +Set `BASE` to the exact origin in the hub's `BETTER_AUTH_BASE_URL`, or sign-up fails the CSRF check. Validate every response with arktype. + +## The install order + +Each step lists, then creates if missing, and adopts what exists: + +1. Sign up, on 422 sign in. Keep the cookie. The app holds a user account as its "system" identity. +2. Find or create the app root tenant by slug in `GET /api/me/principals`. Slugs are global across the hub, so prefix with your app id. On 409, look it up again. +3. Seed roles and grants. +4. Seed inference: provider, credential, model, model provider, offering. Keep the offering ids. +5. Create a workflow asset, mint a short-lived git token, push the workflow package, revoke the token, and read the commit sha. +6. Deploy lazily, when the work first needs it: + +```ts +const d = await deployWorkflow(transport, tenantId, { + source: { kind: "asset", assetId, package: { format: "source", commitSha } }, + entry: "src/workflow.ts", + sourceOfferingIds: offeringIds, + defaultSourceOfferingId: offeringIds[0], +}); +// poll listWorkflowDeployments until status === "deployed" +``` + +7. Run with `triggerWorkflowRun(transport, tenantId, d.id, { content })`. Observe with `use_skill interchange-chat`. + +`deployWorkflow`, `triggerWorkflowRun`, `deliverWorkflowSignal`, `listWorkflowDeployments`, `readWorkflowRunEvents` are the hub-client helpers. The rest are plain `transport.fetch` calls, so write a small typed wrapper per route. + +## Design rules + +- One root tenant per app, one child tenant per project. Catalog, providers, credentials, and placement inherit down. Assets, roles, and grants do not, so filter asset lists by `tenantId` or you adopt the parent's same-named asset. +- Prefix asset and credential names if two apps ever share a tenant. +- Apps share the hub's provisioner pool. Every deployment gets its own sidecar. +- Client-side permission checks only explain. The hub authorizes. +- Secrets never go in responses, logs, or prompts. + +Reference app: `corbitsdev/corbits-solution-builder`, `packages/installer/src/install.ts` and `hub.ts`. + +## Need more + +- Route bodies: `use_skill interchange-hub-api`. +- The hub process itself (embedded or remote): `use_skill interchange-embed-hub`. +- Chat, runs, and approvals: `use_skill interchange-chat`. +- Workflow code you push: `use_skill interchange-workflows`. diff --git a/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md b/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md index 7ba29e3bd..582cb5ea7 100644 --- a/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md @@ -1,70 +1,76 @@ --- name: interchange-run-modes -description: Pick where the Interchange control plane lives (none with runLocal, embedded hub, remote hub) and how sidecars, credentials, and placement work. +description: Choose where an Interchange workflow or agent runs (runLocal, embedded hub, remote hub plus sidecar), and set up sidecars, credentials, and placement. Load when deciding whether you need a hub. user-invocable: false --- # Interchange run modes -The question is where the control plane lives. Your workflow and agent code is the same in every mode. +The question is where the control plane lives. Workflow and agent code is the same in every mode. -| Tier | Control plane | Use it when | -| ----------------------------- | ------------------------------- | ----------------------------------------------------------------------------- | -| `runLocal` / in-process agent | none | Scripts, CLI tools, CI, tests, dev loops, one user, run ends with the process | -| Embedded hub | in your app process | You need hub features but want one local binary (desktop app) | -| Remote hub | a hosted hub, sidecars dial out | Shared team, runs must outlive any one machine, work placed on other hosts | +| Mode | Control plane | Use when | +| --------------------------- | ------------------------- | ----------------------------------------------------------------------------- | +| `runLocal` or `createAgent` | none | Scripts, CLI tools, CI, tests, dev loops, one user, run ends with the process | +| Embedded hub | inside your app | You need hub features in one local binary, such as a desktop app | +| Remote hub | hosted, sidecars dial out | Shared team, runs outlive any machine, work placed on other hosts | -Embedded and remote differ only in a base URL. An app written against the hub `Transport` switches by config (`use_skill interchange-embed-hub`, `use_skill interchange-client-apps`). Moving up from `runLocal` is a deploy, not a rewrite, so start without a hub unless you need one of the reasons below. +Embedded and remote differ only by base URL. Moving up from `runLocal` is a deploy, not a rewrite. Default to no hub, and move up for one of these reasons: -## Skip the hub when - -- Nothing to stand up: no database, keys, auth origin, or sidecar processes, and a cold start in milliseconds. -- State is inspectable and disposable (a directory), with no second source of truth. -- Tests and CI want hermetic runs with scripted inference and no ambient credentials. -- One person starts the run and watches it end. -- HITL is fine while the process lives: `run.signal(name, payload, signalId)` answers an `awaitSignal`. -- You want to avoid depending on the alpha hub surface. - -## Want a control plane when - -- Waits must outlive the process (approvals, `awaitSignal`, timers): the hub parks, persists, and resumes after a crash. `runLocal` has no durability and a crashed run is terminal. -- Credentials are shared and rotated: the hub seals provider keys and freezes a failover chain at deploy. -- Triggers run while you are away: mail, cron, webhooks. -- Several principals need grants, an audit trail, approvals routes, and spend accounting. -- Work should run in a sandbox or on another host (sidecar placement). -- Runs need addresses so people and agents can mail them. +- A wait (`awaitSignal`, approval, timer) must outlive the process. The hub parks, persists, and resumes after a crash. +- Credentials are shared or rotated. The hub seals them and freezes a failover chain at deploy. +- Triggers must fire while you are away (mail, cron, webhooks). +- Several principals need grants, audit, approvals, and spend accounting. +- Work must run in a sandbox or on another host. +- People or agents must mail a run by address. - A deploy must be pinned to a commit. +Hub setup: `use_skill interchange-embed-hub`, `use_skill interchange-hub-setup`. + ## runLocal -`runLocal(definition, { authorize, hasUpstreamSignalResolver, ... })` returns `{ runId, complete, cancel, signal }`. `authorize` and `hasUpstreamSignalResolver` are required; pass `true` for a top-level run you will signal. +Runs the real workflow runtime with in-memory parts. Reference: upstream `examples/workflow-quickstart`. -- Supported: signals, authorize, actions (need `actionResolver`), loops (need `loopFns`), inline child workflows, timers, an in-memory event log in `RunResult.events`, cancel, and `resumeFromEvents` if you feed the log back yourself. -- Not supported: persistence, crash resume, drain, hub approval rows or `/approvals` routes, mail and triggers, tools, hub credentials, placement. -- The default step invoker is a stub that returns `{ output: null }`. To run a model, pass an `invokeStep` that builds `createAgent` and calls `agent.send`. -- An untimed `awaitSignal` inside a `childWorkflow` fails the child, because nothing can answer it. -- Unverified: whether `onTrigger` sections run locally. +```ts +const run = runLocal(workflow, { + authorize: allowAll, // required, no default + hasUpstreamSignalResolver: true, // required, true for a top-level run you will signal + actionResolver, // ref -> ActionHandler, throws on unknown + loopFns, // ref -> LoopFn, throws on unknown + invokeStep: createAgentStepInvoker({ + source, + material, + contextDir, + authorize: allowAll, + }), +}); +await run.signal("go", payload, "sig-1"); // answers an awaitSignal +const result = await run.complete; // { terminalStatus, outputs, events } +``` -For a single agent without a workflow, use in-process `createAgent` with a git-backed store and `@intx/tools-posix` (`use_skill interchange-agents`). You supply `authorize` and audit. +- The default `invokeStep` calls `authorize` and returns `{ output: null }`. It runs no model and no tools. A step that needs an agent needs your `invokeStep` (see `step-invoker.ts` in the quickstart, which builds `createAgent` and calls `send`). +- Works: signals, loops, actions, inline child workflows, timers, cancel, an in-memory event log. +- Missing: persistence, crash resume, `/approvals` routes, mail triggers, hub credentials, placement, mail-addressed runs. A crashed run is terminal. +- An approval park resolves only if you call `run.signal` yourself. +- An untimed `awaitSignal` inside a `childWorkflow` is refused, because nothing can answer it. +- Unverified: whether `onTrigger` sections run locally. -Desktop means a local process: in-process, or an embedded hub that spawns a local sidecar process. There is no dedicated desktop harness. Container, VM, Workers, and browser hosts are in progress, not shipped. +For one agent with no workflow, use `createAgent` with a git-backed `contextDir` and `@intx/tools-posix` (`use_skill interchange-agents`). There is no dedicated desktop harness. Desktop means a local process. Container, VM, Workers, and browser hosts are not shipped. ## Hub plus sidecar -- The hub is the control plane (Postgres, tenants, grants, credentials, deploys, git). A sidecar is a host process that connects outbound over one WebSocket (`HUB_WS_URL`), registers with an allocation token, then runs each deployment as a supervised child (`bin/workflow-child`). It needs no public address. Use `wss://` off loopback. -- Sidecar env: `SIDECAR_DATA_DIR`, `HUB_WS_URL`, `SIDECAR_ID`, `SIDECAR_TOKEN`, `SIDECAR_CREDENTIAL_ENCRYPTION_KEY`. -- Sidecars exist only through a `SidecarProvisioner` (`ensure` and `destroy`, idempotent, generation-fenced). Inject them with `createHubServer({ sidecarProvisioners, probeSidecarProvisioners })`. None is registered by default, so a fresh dev hub cannot probe or deploy. -- A local-process provisioner just spawns `apps/sidecar/src/index.ts` with those env vars. The reference is `tests/admin-ui-e2e/harness/local-process-sidecar-provisioner.ts` upstream. -- A crashed child is respawned with backoff (1 s doubling to 30 s). Three exits in 60 s latch `crash-looping` and fail the run. -- Dev stack: `bin/db-reset && bin/dev --seed` gives the hub on :3000 and the admin UI on :5173. +- The hub is the control plane. A sidecar is a host process that dials the hub over one WebSocket (`HUB_WS_URL`), registers with an allocation token, and runs each deployment as a supervised child. It needs no public address. Use `wss://` off loopback. +- Sidecars exist only through a `SidecarProvisioner` (`ensure`, `destroy`, idempotent, generation-fenced). None is registered by default, so a fresh hub cannot deploy. +- Sidecar env: `HUB_WS_URL`, `SIDECAR_ID`, `SIDECAR_TOKEN`, `SIDECAR_DATA_DIR`, `SIDECAR_CREDENTIAL_ENCRYPTION_KEY`. +- A crashed child restarts with backoff (1 s up to 30 s). Three exits in 60 s mark it `crash-looping` and fail the run. +- Upstream dev stack: `bin/db-reset && bin/dev --seed`, hub on :3000. ## Credentials -Agents never discover credentials. The hub resolves them at launch and the harness mediates use. Inference keys travel inside the `agent.deploy` frame, and rotation pushes `sources.update`. Tool credentials are tenant-owned, decrypted on the hub, and delivered over the control channel. Proactive OAuth refresh is not built. Docs disagree on whether keys sit in `deployment.json`, so check `DEV.md` and the code before relying on either. +Agents never discover credentials. The hub resolves them at launch and the harness mediates use. Inference keys travel in the `agent.deploy` frame and rotation pushes `sources.update`. Tool credentials are tenant-owned and delivered over the control channel. Proactive OAuth refresh is not built. Docs disagree on whether keys sit in `deployment.json`, so check `DEV.md` and the code. ## Placement -A workflow declares what it needs, and a provisioner advertises what it offers: +A workflow declares needs, and a provisioner advertises what it offers: ```ts sidecarPlacement: { @@ -72,11 +78,11 @@ sidecarPlacement: { } ``` -Selectors are exact, `ns:*`, or `*`. Effects are `require` or `block`, and block wins ties. Tenant policy can add constraints that workflows cannot weaken. The OS or container sandbox boundary is not built, so grants are not a syscall or network sandbox. +Selectors are exact, `ns:*`, or `*`. Effects are `require` or `block`, and block wins. Tenant policy adds constraints that workflows cannot weaken. There is no OS or container sandbox yet, so grants are not a syscall or network sandbox. Untrusted work needs a provisioner declaring an `isolation:*` capability. ## Need more -- Upstream docs: `HARNESS_DESIGN.md`, `SIDECAR_PLACEMENT.md`, `CREDENTIALS.md`, `AUTH.md`, `GIT_ACCESS.md`, `unified-execution-host-design.md`. -- Client app driving a hub: `use_skill interchange-client-apps`. Host embedding a hub: `use_skill interchange-embed-hub`. +- Upstream docs: `HARNESS_DESIGN.md`, `SIDECAR_PLACEMENT.md`, `CREDENTIALS.md`, `AUTH.md`, `unified-execution-host-design.md`. +- Apps that drive a hub: `use_skill interchange-client-apps`. - Workflow authoring: `use_skill interchange-workflows`. - Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange-workflows/SKILL.md b/plugins/corbits-skills/skills/interchange-workflows/SKILL.md index e8f44c455..c81e91d61 100644 --- a/plugins/corbits-skills/skills/interchange-workflows/SKILL.md +++ b/plugins/corbits-skills/skills/interchange-workflows/SKILL.md @@ -89,22 +89,22 @@ const result = await run.complete; // { runId, terminalStatus, outputs } There is no primitive that starts or signals another deployment from inside a workflow. Choose by need: -| Need | Use | -| ---------------------------------------- | ------------------------------------------------------------ | -| Human turn into a run | Mail to the run address | -| Human decision into a parked run | `deliverWorkflowSignal` with a stable `signalId` | -| Agent to agent, conversational | `mail_send` to the peer's run address (unverified: triggers) | -| Bounded sub-task, no human | `childWorkflow` (an untimed park inside fails the child) | -| Rework loop with a human gate in one run | `loop` with `awaitSignal` in the body | -| Many human turns in one living run | `onTrigger` section | -| Large or durable content | Artifact or memory reference plus sha256, not the payload | -| Start a different deployment | The client or host (ensure deploy, then trigger) | +| Need | Use | +| ---------------------------------------- | ------------------------------------------------------------------------------ | +| Human turn into a run | Mail to the run address | +| Human decision into a parked run | `deliverWorkflowSignal` with a stable `signalId` | +| Agent to agent, conversational | `mail_send` to the peer's run address (inbound mail to a run address fires it) | +| Bounded sub-task, no human | `childWorkflow` (an untimed park inside fails the child) | +| Rework loop with a human gate in one run | `loop` with `awaitSignal` in the body | +| Many human turns in one living run | `onTrigger` section | +| Large or durable content | Artifact or memory reference plus sha256, not the payload | +| Start a different deployment | The client or host (ensure deploy, then trigger) | Solution Builder orchestrates from the client: it mails per-stage specialists, sends `project.decision` signals to one project workflow, and passes artifact references. On redeploy it starts a new generation and replays recorded decisions through it. ## Need more - Full pitfall list: upstream `docs/WORKFLOW_AUTHORING.md` and `examples/workflow-quickstart`. -- Driving deploys from an app: `use_skill interchange-client-apps`. +- Driving deploys from an app: `use_skill interchange-hub-setup`. Chat and signals in a UI: `use_skill interchange-chat`. - Scheduled or webhook-triggered runs: `use_skill corbits-hub-libs`. - Overview: `use_skill interchange`. diff --git a/plugins/corbits-skills/skills/interchange/SKILL.md b/plugins/corbits-skills/skills/interchange/SKILL.md index b573ea4fb..cfb331865 100644 --- a/plugins/corbits-skills/skills/interchange/SKILL.md +++ b/plugins/corbits-skills/skills/interchange/SKILL.md @@ -36,6 +36,6 @@ State is git: the audit log is the commit history, resume is re-running on the s - Build or embed an agent in code, pick tools, sources, and test it: `use_skill interchange-agents`. - Multi-step, durable, resumable work: `use_skill interchange-workflows`. - Where and how it runs (local, hub plus sidecar, NAT, credentials, placement): `use_skill interchange-run-modes`. -- An app that sets up and drives its own hub (tenants, auth, deploys): `use_skill interchange-client-apps`. Hosting the hub in your process: `use_skill interchange-embed-hub`. +- An app that sets up and drives its own hub: `use_skill interchange-client-apps` (it routes to `interchange-embed-hub`, `interchange-hub-setup`, `interchange-hub-api`, `interchange-chat`). - Corbits packages that plug into it (providers, MCP, cron, memory, UI): `use_skill corbits`. - Design docs in the upstream repo: `docs/ARCHITECTURE.md`, `INFERENCE.md`, `AUTH.md`, `CREDENTIALS.md`, `HARNESS_DESIGN.md`. Package layout: `LAYOUT.md`. Runnable references: `examples/README.md`. diff --git a/src/telemetry/classify.ts b/src/telemetry/classify.ts index 5735fd480..b453a5b68 100644 --- a/src/telemetry/classify.ts +++ b/src/telemetry/classify.ts @@ -90,6 +90,9 @@ const FIRST_PARTY_SKILL_NAMES: ReadonlySet = new Set([ "interchange-agents", "interchange-client-apps", "interchange-embed-hub", + "interchange-hub-api", + "interchange-hub-setup", + "interchange-chat", "interchange-run-modes", "interchange-workflows", "interview",