diff --git a/docs/PRODUCT.md b/docs/PRODUCT.md index 3650c8727..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. `/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/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-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 new file mode 100644 index 000000000..a1c8882e1 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-client-apps/SKILL.md @@ -0,0 +1,40 @@ +--- +name: interchange-client-apps +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 + +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. + +## Find your task + +| 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` | + +## Shape + +- **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 + +- 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 + +- 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 new file mode 100644 index 000000000..a3384f312 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-embed-hub/SKILL.md @@ -0,0 +1,56 @@ +--- +name: interchange-embed-hub +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 +--- + +# Hosting an Interchange hub + +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: + +| 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 | + +A local app should switch on one setting. Unset means embedded, set means remote. Never migrate against a remote hub. + +## Stock hub + +`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/`: + +```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))); +``` + +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. + +## Own composition + +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`. + +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. + +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. + +## Rules that bite + +- 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 + +- 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`. 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 new file mode 100644 index 000000000..582cb5ea7 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-run-modes/SKILL.md @@ -0,0 +1,88 @@ +--- +name: interchange-run-modes +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. Workflow and agent code is the same in every mode. + +| 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 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: + +- 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 + +Runs the real workflow runtime with in-memory parts. Reference: upstream `examples/workflow-quickstart`. + +```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 } +``` + +- 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. + +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. 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 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 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. 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`, `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 new file mode 100644 index 000000000..c81e91d61 --- /dev/null +++ b/plugins/corbits-skills/skills/interchange-workflows/SKILL.md @@ -0,0 +1,110 @@ +--- +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`. + +## 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 (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-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 new file mode 100644 index 000000000..cfb331865 --- /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`. +- 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 faf49c072..b453a5b68 100644 --- a/src/telemetry/classify.ts +++ b/src/telemetry/classify.ts @@ -86,6 +86,15 @@ const FIRST_PARTY_SKILL_NAMES: ReadonlySet = new Set([ "docs", "git-worktrees", "implement", + "interchange", + "interchange-agents", + "interchange-client-apps", + "interchange-embed-hub", + "interchange-hub-api", + "interchange-hub-setup", + "interchange-chat", + "interchange-run-modes", + "interchange-workflows", "interview", "issue", "plan",