diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index 40aab28dc..d78cab40d 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -19,7 +19,7 @@ lower-level name left to list. | **Definition** | workflow definition | A deployable unit of agent behavior, authored as code — not a template you mint into many conversations | | **Workbench Definition** | — | The arktype description a named template instantiates: its default agents, routines, tools, required/optional plugins, and its ordered onboarding walkthrough — see `packages/workflow-catalog/src/templates.ts`'s `WorkbenchDefinitionSchema` | | **Run** | workflow run | A definition executing in a bench; interactive runs carry conversations | -| **Routine** | — | The named parent entity over runs of one definition — a trigger (or none), a delivery workbench, and its run history; see [`@corbits/routines`](../packages/routines/README.md) | +| **Routine** | — | The named parent entity over runs of one definition — a trigger (or none), launch input, a delivery workbench, enabled state, and an explicit target: the definition's asset id, resolved at each launch to its latest approved deployment; see [`@corbits/routines`](../packages/routines/README.md) and [workflow-model.md](workflow-model.md) | | **Approval** | approval | A human decision gating an external side effect | | **Grant** | grant | Permission for a principal to act on a resource | | **Hub** | hub | The API and coordination service a bench lives on | diff --git a/docs/workflow-model.md b/docs/workflow-model.md new file mode 100644 index 000000000..262575d6a --- /dev/null +++ b/docs/workflow-model.md @@ -0,0 +1,113 @@ +# Workflows, agents, routines, and runs — the canonical model + +Contract for CL-7349. Read against the vendored pin `a8bc06ae` and upstream +`faremeter/interchange` origin/main `d187e327` (2026-09-01). Every +implementer of the Routines & Workflow Alignment project builds to this +document; a surface that disagrees with it is wrong until this document +changes. + +## Five nouns, one execution model + +| Noun | What it is | Where it lives | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| **Workflow source** | A versioned code package: `package.json` declaring `interchange.workflow` plus the entry module it names, default-exporting a `WorkflowDefinition` | A tenant-scoped `kind: "workflow"` asset in the hub's git-backed asset store (`AssetService`) | +| **Deployed definition** | The frozen, approved projection of one source commit: `workflow_definition` row with non-null `approved_wire_hash`, `grant_snapshot`, `wire_projection`, `status: deployed` | Produced only by Interchange's source pipeline (`deployWorkflowFromSource`: bundle → sidecar probe → capability walk → gate → freeze) | +| **Agent** | A single-step conversational workflow. A product category, not an execution primitive | Same asset + same deployed definition as any workflow | +| **Routine** | Trigger, launch input, delivery configuration, enabled state, and an explicit reference to a definition | `@corbits/routines` (`routine` table) | +| **Run** | One execution of an approved deployed definition | `workflow_run`, launched by `launchAndCorrelate` | + +`workflow.json` is retired. It is not an authoring format, not a +compatibility format, and no path may read or write it. The push validator +(`vendor/intx/hub-sessions/src/workflow-kind.ts`) refuses it; +`@corbits/workflow-source`'s `RetiredWorkflowEnvelopeError` is the only +remaining mention, and it exists to reject. + +## Definition identity and the follow-latest rule + +Interchange keys `workflow_definition` on `(asset_id, wire_hash)` +(`vendor/intx/hub-sessions/src/workflow-definition-ensure.ts`). Every +redeploy of a source asset whose probed wire hash differs mints a **new** +definition row. There is no native "definition id → newest approved +deployment" indirection; every native launch names an exact definition. + +Product ruling (2026-09-01): a routine follows its target's latest approved +deployment and does not pin the version selected at creation. Under the +identity above that means: + +- A routine stores the **definition asset id** (`routine.definition_asset_id`, + `NOT NULL`). The asset is the stable identity of a workflow across + redeploys. +- At launch, the routine's target resolves to the newest `workflow_definition` + row for that asset with `status = 'deployed'` and non-null + `approved_wire_hash`, `grant_snapshot`, and `wire_projection`. Resolution + is atomic with the launch and fails closed when no such row exists. +- Routine reads return both the asset id (identity) and the currently + resolved definition id (what would run now) so UI and Myra can show them. + +Nothing in Workbench pins a wire hash or copies a projection into routine +storage. + +## Authority boundaries + +| Operation | Canonical operation | Authorized as | Human approval | +| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| Store source (create / republish) | `@corbits/agent-workflow-authoring` registry → `AssetService.createAsset` / `populateAsset` (hub-signed commit) | Initiating tenant + principal; `@intx/authz` `authorize` on `asset:*`/`create` or `asset:`/`write` | None (writing source is not a side effect) | +| Deploy source | `POST /api/tenants/:tenantId/workflows/deployments` → vendored `SessionService.deployWorkflowFromSource` | Tenant session or run bearer; `workflow:*`/`create` | Agent-initiated deploys go through an `approval: "ask"` tool call carrying the probed capability surface (below) | +| Create / update a routine | `createRoutineRoutes` `POST /routines`, `PATCH /routines/:id`; the run-authenticated mirror `createWorkflowRoutineRoutes` delegates to the same store | Tenant + principal; target validated against the resolution rule above before persisting | None; a routine only references a definition asset — nothing executes at create/update time | +| Launch | `launchAndCorrelate` (`packages/routines/src/routes.ts`) → hub `RoutineLauncher` | Routine's tenant; grants materialized by the native launch path | Runtime tool calls with `approval: "ask"` park on the native `approval` resource | +| Approve | Native `POST /api/tenants/:tenantId/approvals/:id/approve` | A principal holding `approval:*`/`resolve` — a human; no agent holds it | This is the approval | + +Credentials and resolved provider secrets never enter source trees, deploy +requests recorded by `@corbits/workflow-deploy-source`, or routine rows. +Inference sources are re-resolved from the tenant catalog at deploy and +redeploy (`resolveDefinitionSources`). + +### Deploy approval for agent-authored workflows + +Upstream's deploy route freezes with `approvals: { mode: "approve-probed" }` +(`vendor/intx/hub-sessions/src/session-service.ts`); the `ApprovalSet` +gate exists as a policy type but has no pending-approval record. The only +native pending-approval store is the runtime `approval` resource that an +`approval: "ask"` tool call parks on. Workbench composes those two seams and +adds no approval table: + +1. Myra calls a preview operation that runs the native probe with an empty + `ApprovalSet` and returns the walked grant surface plus the wire hash. No + freeze. +2. Myra calls `workflow_deploy` (`approval: "ask"`) with the asset id, + commit sha, expected wire hash, and that grant list. The tool call parks; + the human sees exactly what will be approved. +3. On approval the tool posts to the native deployments route. The native + probe re-runs; a wire hash that differs from the approved one fails + closed. Rejection leaves the source intact and the definition + unlaunchable. + +Myra cannot resolve approvals: `approval:*`/`resolve` is never minted for an +agent principal. + +## Behavior to delete, not retain + +- Routine target inference from chat membership: `resolveCreateTarget` in + `apps/web/src/shell/routine-panel.tsx` (`agents[0]?.definitionId`, + `ensureMyraWorkbench` fallback) and the "no agent invited" guards around + it. +- Hub-local self-freeze of agent-authored definitions: + `packages/agent-directory`'s use of `@corbits/workflow-freeze` + `DefinitionFreezer`, and the template-block inert freeze in + `apps/hub/src/index.ts`, once those callers deploy natively. +- Duplicate routine wire shapes in `packages/routines-tools/src/client.ts` + (use `@corbits/routines/client`). +- Any code path that reads or writes `workflow.json`. + +No compatibility shim, feature flag, or dual-write period accompanies any of +these deletions. + +## What is not native, and stays in Workbench + +Checked against upstream origin/main `d187e327`: Interchange has no +routine or scheduler (its `ScheduleTrigger` type has zero consumers), no +per-principal "launchable definitions" query, and no agent-facing tool that +writes assets. Those three compositions are Workbench's, built over native +rows and `@intx/authz`. Before adding anything else, check upstream first; +if upstream has it past our pin, re-vendor at that commit rather than +reimplementing. diff --git a/docs/workflow-source-authoring.md b/docs/workflow-source-authoring.md new file mode 100644 index 000000000..f9d1c26e5 --- /dev/null +++ b/docs/workflow-source-authoring.md @@ -0,0 +1,131 @@ +# Authoring workflow source as a hub asset + +Contract for CL-7352: how an agent (Myra) or a person writes a workflow code +package into the hub and gets it deployed through Interchange's native +pipeline. Companion to [workflow-model.md](workflow-model.md). + +## The package shape + +A `kind: "workflow"` asset carries an ordinary code package. The push +validator (`vendor/intx/hub-sessions/src/workflow-kind.ts`, +`workflowKindHandler.validatePush`) accepts nothing else. + +``` +package.json — name, version, "type": "module", "interchange": { "workflow": "./workflow.ts" } +workflow.ts — default-exports defineWorkflow({ ... }) from @intx/workflow + — anything the entry imports; no secrets, no .env +``` + +Rules the authoring boundary enforces before any write: + +- Paths are repo-relative, normalized, no `..`, no leading `/`, no + `.git/`. Names matching secret-like patterns (`.env*`, `*.pem`, `*.key`, + `id_rsa*`) are rejected. +- `package.json` parses, declares a non-empty `interchange.workflow`, and + that entry exists in the same tree. +- Total tree size and per-file size are capped. +- The asset name is lowercase-kebab (`ASSET_NAME_PATTERN`). + +Inert JSON definitions (`export default {...}` written by +`renderWorkflowSourceTree`) remain a valid package shape for hub-generated +single-step agents, because they are code the sidecar evaluates. They are +not what an agent authors by hand. + +## The operations, in order + +| Step | Operation | Authorized as | Returns | +| ---- | ----------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------- | +| 1 | `POST /api/workflow-workflow-authoring/author` (`@corbits/agent-workflow-authoring`) → `AssetService.createAsset` + `populateAsset` | Run bearer + run address → tenant/principal; `asset:*`/`create` | `{ assetId, name, commitSha }` | +| 1' | `.../republish` → `populateAsset` on `refs/heads/main` | `asset:`/`write`, own-tenant row check first | `{ assetId, name, commitSha }` | +| 2 | Preview: native probe with empty `ApprovalSet`, no freeze | Same run scope | `{ wireHash, grants[] }` or an invalid-package error | +| 3 | `POST /api/tenants/:tenantId/workflows/deployments` | `workflow:*`/`create`; parked on `approval: "ask"` when agent-initiated | `WorkflowDeploymentResponse { id, definitionAssetId, status }` | +| 4 | Human resolves the parked approval (native `approvals` route) | `approval:*`/`resolve` | Deploy continues or is rejected | +| 5 | `workflow_definition` row frozen; appears in routine target discovery | — | Launchable | + +The deploy body is the same one `packages/hub-client/src/seed.ts` sends: + +```json +{ + "source": { + "kind": "asset", + "assetId": "", + "package": { "format": "source", "commitSha": "" } + }, + "entry": "./workflow.ts", + "sources": [ + { + "id": "...", + "provider": "...", + "baseURL": "...", + "apiKey": "...", + "model": "..." + } + ], + "defaultSource": "..." +} +``` + +`sources` is resolved server-side from the tenant's inference catalog for +agent-initiated deploys; an agent never supplies or sees provider secrets. +`commitSha` is the pin: the same asset at a different commit is a different +deploy. `@corbits/workflow-deploy-source` records `{ assetId, commitSha, +entry }` per placement so redeploy re-resolves sources fresh from the +recorded initiating principal. + +## Identity, conflicts, idempotency + +- Asset identity is the asset id; the human-readable name is unique per + tenant (`duplicate_asset` → 409 conflict). +- A republish carries `expectedHeadSha`. If the ref moved, the write is + rejected with 409 and the current head; the caller re-reads and retries. + Nothing is silently overwritten. +- Writing an identical tree is a no-op commit (content-aware, like the CLI + pusher). Retrying an `author` after a network failure hits + `duplicate_asset`; the caller then republishes. +- An authored-but-never-deployed asset is a draft by state, not by table: + it has no `workflow_definition` row. It stays in the asset store until + deleted; it never appears in routine target discovery. +- Every operation is authorized as the run's own tenant and principal + (`WorkflowRunAuthenticator`); no tool argument names a tenant or asset it + cannot already reach. + +## Sequence + +```mermaid +sequenceDiagram + participant H as Human + participant M as Myra (run) + participant A as agent-workflow-authoring + participant S as AssetService (git) + participant D as /workflows/deployments + participant P as Sidecar probe + participant R as Routine targets + + H->>M: "make a routine that does X" + M->>A: author { name, files } + A->>A: authorize asset:*/create, validate paths + package + A->>S: createAsset + populateAsset (hub-signed commit) + S-->>M: { assetId, commitSha } + M->>A: deploy preview { assetId, commitSha } + A->>P: probe (empty ApprovalSet, no freeze) + P-->>M: { wireHash, grants } + M->>M: workflow_deploy (approval: ask) parks + H->>H: inspects asset, commit, grants; approves + M->>D: POST { source: asset/source/commitSha, entry } + D->>P: bundle, probe, capability walk + P-->>D: wireHash must equal approved; else fail closed + D->>D: freeze workflow_definition (approved_wire_hash, grant_snapshot) + D-->>M: deployment { definitionAssetId } + R-->>H: definition selectable as routine target +``` + +## Seams that do not exist yet (and where they go) + +- An `@intx/agent` tool bundle over the authoring routes + (`@corbits/workflow-authoring-tools`): CL-7360. +- A run-authenticated preview route and the `workflow_deploy` tool: CL-7361, + CL-7362. +- Path/package validation in `agent-workflow-authoring`'s registry: + CL-7360. + +Nothing here adds a repository, compiler, probe, freezer, or approval store.