From 044d746334f3bb9fa1b5fd54cbd610b79c2338eb Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 11:40:53 -0700 Subject: [PATCH 1/5] chore(workflows): delete dead launch, projection and detail modules (CL-9641) --- docs/agent-session-provisioning.md | 27 +--- docs/workflow-definition-projection.md | 35 ---- e2e/workflows/agent-session.test.ts | 80 ++------- packages/workflows/src/client.ts | 15 -- .../workflows/src/definition-projection.ts | 153 ------------------ .../workflows/src/deliver-when-routable.ts | 51 ------ .../workflows/src/detail/definition-detail.ts | 31 ---- packages/workflows/src/detail/index.ts | 7 - packages/workflows/src/index.ts | 31 +--- .../workflows/src/launch/agent-session.ts | 76 +-------- .../workflows/src/launchable/target-rule.ts | 127 --------------- packages/workflows/src/source.ts | 2 - 12 files changed, 25 insertions(+), 610 deletions(-) delete mode 100644 docs/workflow-definition-projection.md delete mode 100644 packages/workflows/src/definition-projection.ts delete mode 100644 packages/workflows/src/deliver-when-routable.ts delete mode 100644 packages/workflows/src/detail/definition-detail.ts delete mode 100644 packages/workflows/src/detail/index.ts delete mode 100644 packages/workflows/src/launchable/target-rule.ts diff --git a/docs/agent-session-provisioning.md b/docs/agent-session-provisioning.md index fd12b9e945..975511688a 100644 --- a/docs/agent-session-provisioning.md +++ b/docs/agent-session-provisioning.md @@ -4,14 +4,13 @@ Notes on `@corbits/workflows`'s `launch/agent-session.ts`. ## Why this module exists -Every Workbench launch path provisions through Interchange's -`prepareProvisionedDeployment` with a `sessionId` it mints itself, but +Interchange's `prepareProvisionedDeployment` mints a `sessionId`, but nothing writes that id into `agent_session` — the table -`resolveRunSessionId` reads to route a run's outbound mail. Every launcher -must do this itself, through this one shared helper, so mail and spans -persist against a real session from the run's first turn. +`resolveRunSessionId` reads to route a run's outbound mail. +`ensureRunSession` does, so mail and spans persist against a real session +from the run's first turn. -## Why timing forces a two-step write +## Why the write waits for the run principal `workflow_run.principal_id` (the FK `agent_session` keys on) is still null the instant `prepareProvisionedDeployment` returns — a provisioned anchor is @@ -22,16 +21,6 @@ principal row `onConflictDoNothing` and treats a conflict as "grants already committed," throwing rather than re-materializing — so nothing upstream of that first trigger may write the principal first. -`recordAgentSessionAtProvision` writes the row immediately after -`prepareProvisionedDeployment` returns, keyed on the deploying principal -since the run principal doesn't exist yet. `ensureRunSession` re-keys onto -the run's own principal once one is anchored, called from every seam that -might be a run's first mail-routable moment, and is a true upsert so a run -deployed straight through Interchange's own deployments route (which skips -the eager write) still gets a row. - -## `endAgentSessionFor*` - -Called only from places that already know a run died — a chat relaunch or a -one-shot prompt's teardown. No sweeper; a run nobody reacts to stays -`active` until something does. +`ensureRunSession` is a true upsert, called from every seam that might be a +run's first mail-routable moment: it creates the row once a principal is +anchored, and re-keys an existing row onto it. diff --git a/docs/workflow-definition-projection.md b/docs/workflow-definition-projection.md deleted file mode 100644 index f473812071..0000000000 --- a/docs/workflow-definition-projection.md +++ /dev/null @@ -1,35 +0,0 @@ -# Workflow definition projection - -Notes on `@corbits/workflows`'s `definition-projection.ts`, the pure reader -that turns an already-resolved inert wire projection into a `FoldedBody`. - -## Why this exists - -There's no hub-side row to read a launch body back off of. This module is -the pure half: the schema-validated reader for anything holding a -projection value from elsewhere (chat, in particular). Nothing here queries -a database. - -`grantRequirements` is deliberately excluded from the projection: it does -not survive the live-to-inert projector and lives instead on the -`workflow_definition.grant_requirements` column, so callers pass it in -alongside the projection rather than reading it off. - -## Step shapes - -An inert projection's launch step comes in two shapes: a bare `step` (the -conversational case) or an `onTrigger` section whose inline body carries the -one agent-bearing step that answers each turn. `extractAgentBearingStep` -reads through either shape with one reader, so neither call site duplicates -the other's parsing. - -## Why multi-step definitions are rejected - -The launch target (the bundled worker deploy in -`apps/web/src/agent-deploy.ts`) renders exactly one `systemPrompt` into one -mailbox-triggered turn — it has no notion of step order. Reading past -`stepOrder[0]` would silently drop every later step's behavior rather than -run it, so `MultiStepFoldUnsupportedError` is thrown instead. Genuine -multi-step launch needs a different deploy front (Interchange's native -workflow-run trigger, `@intx/workflow-host`'s DAG supervisor) than this -module provides. diff --git a/e2e/workflows/agent-session.test.ts b/e2e/workflows/agent-session.test.ts index 2e9fec07b6..d9a26aaae4 100644 --- a/e2e/workflows/agent-session.test.ts +++ b/e2e/workflows/agent-session.test.ts @@ -2,19 +2,12 @@ // repo's convention for tests that talk to a real Postgres (see // `tools/access/test/routes.test.ts`). // -// `recordAgentSessionAtProvision` is the eager write every -// native launcher makes right after `prepareProvisionedDeployment` -// returns — this proves it lands the `agent_session` row under the -// *deploying* principal (the run's own principal does not exist yet) -// and creates the run's event collector exactly once, idempotently. // `ensureRunSession` is a true upsert: once the run's first trigger -// anchors a real principal onto it, this proves the session moves onto -// that principal without ever touching the session id when a row -// already exists, that it creates the row from scratch for a run -// deployed straight through Interchange's own deployments route (no -// Workbench launcher ever ran `recordAgentSessionAtProvision` for it), -// and that a run with no launch spec row is a bug this throws on rather -// than papering over. +// anchors a real principal onto it, this proves the session is created on +// that principal keeping the launch spec's session id, that it works for a +// run deployed straight through Interchange's own deployments route, and +// that a run with no launch spec row is a bug this throws on rather than +// papering over. import { afterAll, beforeAll, expect, test } from "bun:test"; import { eq } from "drizzle-orm"; @@ -23,10 +16,7 @@ import { generateId } from "@intx/hub-common"; import { resolveRunSessionId } from "@intx/hub-sessions"; import { dbGate } from "../lib/db-gate"; -import { - ensureRunSession, - recordAgentSessionAtProvision, -} from "../../packages/workflows/src/launch/agent-session"; +import { ensureRunSession } from "../../packages/workflows/src/launch/agent-session"; function dbConfigFromUrl(databaseUrl: string) { const url = new URL(databaseUrl); @@ -42,7 +32,7 @@ function dbConfigFromUrl(databaseUrl: string) { const databaseUrl = process.env["DATABASE_URL"]; const describeIfDb = dbGate(databaseUrl, import.meta.path); -describeIfDb("recordAgentSessionAtProvision / ensureRunSession", () => { +describeIfDb("ensureRunSession", () => { let db: DB; const tenantId = generateId("tenant"); @@ -102,9 +92,8 @@ describeIfDb("recordAgentSessionAtProvision / ensureRunSession", () => { name: "agent-session-test-definition", }); - // Born the instant `prepareProvisionedDeployment` returns: a run row - // exists, its own principal is still null, and its launch spec names - // the session `recordAgentSessionAtProvision` will record. + // A run row whose own principal is not yet anchored; its launch spec + // names the session. await db.db.insert(schema.workflowRun).values({ id: provisionedRunId, definitionId, @@ -138,12 +127,10 @@ describeIfDb("recordAgentSessionAtProvision / ensureRunSession", () => { status: "deployed", }); - // Deployed through Interchange's own deployments route rather than a - // Workbench launcher: Interchange itself writes the launch spec (it - // mints the session id), but nothing ever calls - // `recordAgentSessionAtProvision` for this path, so no `agent_session` - // row exists yet. Already anchored with its own principal by the time - // `ensureRunSession` is reached, same as at persist/dispatch. + // Deployed through Interchange's own deployments route: Interchange + // writes the launch spec (it mints the session id), so no + // `agent_session` row exists yet. Already anchored with its own + // principal by the time `ensureRunSession` is reached. await db.db.insert(schema.workflowRun).values({ id: apiDeployedRunId, definitionId, @@ -187,46 +174,7 @@ describeIfDb("recordAgentSessionAtProvision / ensureRunSession", () => { await db.db.delete(schema.tenant).where(eq(schema.tenant.id, tenantId)); }); - test("records the session under the deploying principal and creates the collector once", async () => { - const createCalls: unknown[] = []; - let hasCollector = false; - const eventCollectors = { - create: (...args: unknown[]) => { - createCalls.push(args); - hasCollector = true; - }, - has: () => hasCollector, - }; - - await recordAgentSessionAtProvision({ - db: db.db, - eventCollectors, - runId: provisionedRunId, - sessionId, - sourceAuthorityPrincipalId: deployingPrincipalId, - }); - // A second call for the same run is a no-op, not a conflict, and - // must not create a second collector. - await recordAgentSessionAtProvision({ - db: db.db, - eventCollectors, - runId: provisionedRunId, - sessionId, - sourceAuthorityPrincipalId: deployingPrincipalId, - }); - - expect(createCalls).toEqual([ - [`${provisionedRunId}@${domain}`, tenantId, sessionId, provisionedRunId], - ]); - - const sessionRow = await db.db.query.agentSession.findFirst({ - where: eq(schema.agentSession.id, sessionId), - }); - expect(sessionRow?.principalId).toBe(deployingPrincipalId); - expect(sessionRow?.agentId).toBe(definitionId); - }); - - test("re-keys the session onto the run principal once Interchange anchors it", async () => { + test("creates the session on the run principal once Interchange anchors it", async () => { await db.db .update(schema.workflowRun) .set({ principalId: runPrincipalId, status: "running" }) diff --git a/packages/workflows/src/client.ts b/packages/workflows/src/client.ts index fc7d3f9498..f5ba1b36be 100644 --- a/packages/workflows/src/client.ts +++ b/packages/workflows/src/client.ts @@ -1,21 +1,6 @@ // @corbits/workflows browser-safe entry — no `@intx/*`, no `drizzle-orm`, // no `hono`. A structural check walks the import graph from here. export * from "./source"; -export { - pickLaunchableDefinition, - isFrozen, - routineTargetRejection, - RoutineTargetUnresolvableError, - type LaunchableDefinitionCandidate, - type LaunchableDefinitionRejection, - type LaunchableDefinitionResolution, - type LaunchableDefinitionResolver, -} from "./launchable/target-rule"; -export { - workflowNotLaunchableReason, - workflowDetailPath, - WorkflowDefinitionDetail, -} from "./detail/definition-detail"; export { WorkflowTriggerField, WORKFLOW_CATALOG, diff --git a/packages/workflows/src/definition-projection.ts b/packages/workflows/src/definition-projection.ts deleted file mode 100644 index 8648cdd612..0000000000 --- a/packages/workflows/src/definition-projection.ts +++ /dev/null @@ -1,153 +0,0 @@ -// Reads a `WorkflowDefinition`'s launch body back out of an already-resolved -// inert wire projection. See docs/workflow-definition-projection.md. -import type { FoldedBody } from "@intx/workflow-deploy"; -import { GrantRequirement, CredentialBinding } from "@intx/types"; -import { ToolPackagePin } from "@intx/types/tool-packages"; -import { type } from "arktype"; - -/** Thrown when a definition carries no frozen wire projection. Carries - * consumer-facing `guidance` so an HTTP boundary can answer 4xx, not 500. */ -export class DefinitionProjectionMissingError extends Error { - readonly definitionName: string; - readonly guidance: string; - constructor(definitionName: string) { - const guidance = - "This agent isn't finished setting up. Open it in Agents, save " + - "its instructions, and try again — or recreate it."; - super(`No stored launch body for definition "${definitionName}" (${guidance})`); - this.name = "DefinitionProjectionMissingError"; - this.definitionName = definitionName; - this.guidance = guidance; - } -} - -/** One definition candidate, ordered newest-first by the caller - * (typically `createdAt desc`). */ -export type DefinitionCandidate = { - readonly id: string; - readonly name: string; -}; - -/** Thrown when a definition's frozen projection carries more than one step — - * this launch target has no notion of step order. See - * docs/workflow-definition-projection.md#why-multi-step-definitions-are-rejected. */ -export class MultiStepFoldUnsupportedError extends Error { - readonly definitionId: string; - readonly stepCount: number; - readonly guidance: string; - constructor(definitionId: string, stepCount: number) { - const guidance = - "This agent has multiple workflow steps, which this launch path " + - "does not yet support — only its first step would run. Reduce it " + - "to a single step, or wait for multi-step routine launch support."; - super( - `definition ${definitionId} is not single-step (${String(stepCount)} steps) (${guidance})`, - ); - this.name = "MultiStepFoldUnsupportedError"; - this.definitionId = definitionId; - this.stepCount = stepCount; - this.guidance = guidance; - } -} - -/** The launch-relevant subset of an inert projection's `step` primitive, - * after the projector flattens the live `AgentDefinition`'s inference chain. */ -const InertWorkflowStepSchema = type({ - kind: "'step'", - agent: { - systemPrompt: "string", - "toolPackagePins?": ToolPackagePin.array(), - modelSources: type({ model: "string" }).array(), - }, -}); - -/** The launch-relevant subset of an inert projection's `onTrigger` - * primitive — the agent-bearing step lives inside the section's inline body. */ -const InertOnTriggerStepSchema = type({ - kind: "'onTrigger'", - body: { - inline: { - stepOrder: "string[]", - steps: "Record", - }, - }, -}); - -/** Extracts the agent-bearing step, whichever of the two launch-step shapes - * a projection takes. See docs/workflow-definition-projection.md#step-shapes. */ -function extractAgentBearingStep( - rawStep: unknown, - definitionId: string, - stepId: string, -): typeof InertWorkflowStepSchema.infer { - const asStep = InertWorkflowStepSchema(rawStep); - if (!(asStep instanceof type.errors)) { - return asStep; - } - const asSection = InertOnTriggerStepSchema(rawStep); - if (asSection instanceof type.errors) { - throw new Error( - `definition ${definitionId} step ${stepId} is not a step primitive: ${asStep.summary}`, - ); - } - const body = asSection.body.inline; - const [bodyStepId, ...bodyRest] = body.stepOrder; - if (bodyStepId === undefined || bodyRest.length > 0) { - throw new Error( - `definition ${definitionId} section ${stepId}'s body is not ` + - `single-step (${String(body.stepOrder.length)} steps)`, - ); - } - const bodyStep = InertWorkflowStepSchema(body.steps[bodyStepId]); - if (bodyStep instanceof type.errors) { - throw new Error( - `definition ${definitionId} section ${stepId} body step ` + - `${bodyStepId} is not a step primitive: ${bodyStep.summary}`, - ); - } - return bodyStep; -} - -/** The launch-relevant subset of an inert projection itself. */ -const InertWorkflowDefinitionSchema = type({ - id: "string", - stepOrder: "string[]", - steps: "Record", - "credentialBindings?": CredentialBinding.array(), -}); - -export const FoldedBodySchema = type({ - systemPrompt: "string", - toolPackagePins: ToolPackagePin.array(), - grantRequirements: GrantRequirement.array(), - credentialBindings: CredentialBinding.array(), - model: "string | null", -}); - -/** Reads the launch body off a frozen inert projection instead of a live - * `WorkflowDefinition`. `grantRequirements` comes from the caller since the - * projector drops it. */ -export function readFoldedBody(projection: unknown, grantRequirements: unknown): FoldedBody { - const definition = InertWorkflowDefinitionSchema(projection); - if (definition instanceof type.errors) { - throw new Error(`inert projection is malformed: ${definition.summary}`); - } - const [stepId, ...rest] = definition.stepOrder; - if (stepId === undefined || rest.length > 0) { - throw new MultiStepFoldUnsupportedError(definition.id, definition.stepOrder.length); - } - const step = extractAgentBearingStep(definition.steps[stepId], definition.id, stepId); - const foldedBody = FoldedBodySchema({ - systemPrompt: step.agent.systemPrompt, - toolPackagePins: step.agent.toolPackagePins ?? [], - grantRequirements: grantRequirements ?? [], - credentialBindings: definition.credentialBindings ?? [], - model: step.agent.modelSources[0]?.model ?? null, - }); - if (foldedBody instanceof type.errors) { - throw new Error( - `definition ${definition.id} produced an invalid folded body: ${foldedBody.summary}`, - ); - } - return foldedBody; -} diff --git a/packages/workflows/src/deliver-when-routable.ts b/packages/workflows/src/deliver-when-routable.ts deleted file mode 100644 index f278320df0..0000000000 --- a/packages/workflows/src/deliver-when-routable.ts +++ /dev/null @@ -1,51 +0,0 @@ -// A freshly launched run's first mail can arrive before its sidecar -// finishes booting and registering, failing "agent is unreachable" even -// though nothing is wrong. This helper retries once the address routes. -export const DEFAULT_ROUTABLE_DEADLINE_MS = 20_000; -const DEFAULT_POLL_INTERVAL_MS = 250; - -export function isAgentUnreachableError(err: unknown): boolean { - return err instanceof Error && err.message.includes("agent is unreachable"); -} - -export type DeliverWhenRoutableOptions = { - /** Performs the actual delivery. Called once, and again after the - * address becomes routable if the first attempt fails unreachable. */ - send: () => Promise; - /** Reads the hub's live routing table for this delivery's address. */ - isRoutable: () => boolean; - /** Defaults to matching Interchange's "agent is unreachable" message. */ - isUnreachable?: (err: unknown) => boolean; - /** How long to keep polling before giving up. Defaults to a budget a - * touch past the process provisioner's own boot-and-register time. */ - deadlineMs?: number; - pollIntervalMs?: number; - sleep?: (ms: number) => Promise; -}; - -function defaultSleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -/** Attempts `send`; on "agent is unreachable", polls until routable (or the - * deadline passes) and sends once more. Never swallows the original error. */ -export async function deliverWhenRoutable(opts: DeliverWhenRoutableOptions): Promise { - const isUnreachable = opts.isUnreachable ?? isAgentUnreachableError; - try { - return await opts.send(); - } catch (err) { - if (!isUnreachable(err)) { - throw err; - } - const deadline = Date.now() + (opts.deadlineMs ?? DEFAULT_ROUTABLE_DEADLINE_MS); - const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS; - const sleep = opts.sleep ?? defaultSleep; - while (!opts.isRoutable()) { - if (Date.now() >= deadline) { - throw err; - } - await sleep(pollIntervalMs); - } - return await opts.send(); - } -} diff --git a/packages/workflows/src/detail/definition-detail.ts b/packages/workflows/src/detail/definition-detail.ts deleted file mode 100644 index 0fc24fdefc..0000000000 --- a/packages/workflows/src/detail/definition-detail.ts +++ /dev/null @@ -1,31 +0,0 @@ -// The wire shape a workflow's own page reads. Pure/browser-safe. Mirrors -// `@intx/hub-api`'s stock definitions list, which exposes only these fields. -import { type } from "arktype"; - -export const WorkflowDefinitionDetail = type({ - definitionId: "string", - name: "string", - "description?": "string | null", - status: "'deployed' | 'stopped'", - currentVersion: "string", - createdAt: "string", - updatedAt: "string", -}); -export type WorkflowDefinitionDetail = typeof WorkflowDefinitionDetail.infer; - -/** The next honest action for a definition that is not launchable right - * now. `null` for `deployed`: nothing to say, the strip does not render. - * Stock only distinguishes `deployed`/`stopped` — the richer - * source-only/pending-approval/superseded/build-failed vocabulary the - * native freeze read used to derive is gone with it. */ -export function workflowNotLaunchableReason( - status: WorkflowDefinitionDetail["status"], -): string | null { - return status === "deployed" - ? null - : "This workflow is stopped — resume it to make it launchable."; -} - -export function workflowDetailPath(definitionId: string): string { - return `/workflows/${encodeURIComponent(definitionId)}`; -} diff --git a/packages/workflows/src/detail/index.ts b/packages/workflows/src/detail/index.ts deleted file mode 100644 index d02a9eeffa..0000000000 --- a/packages/workflows/src/detail/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Re-exported from `../client`, not redeclared, so the two barrels can't -// drift apart. -export { - workflowNotLaunchableReason, - workflowDetailPath, - WorkflowDefinitionDetail, -} from "../client"; diff --git a/packages/workflows/src/index.ts b/packages/workflows/src/index.ts index 36e6113100..dd9897493e 100644 --- a/packages/workflows/src/index.ts +++ b/packages/workflows/src/index.ts @@ -1,16 +1,6 @@ // @corbits/workflows server entry. Browser code imports // `@corbits/workflows/client` instead (see ./client.ts). export * from "./source"; -export * from "./detail/index"; -export { - pickLaunchableDefinition, - routineTargetRejection, - RoutineTargetUnresolvableError, - type LaunchableDefinitionCandidate, - type LaunchableDefinitionRejection, - type LaunchableDefinitionResolution, - type LaunchableDefinitionResolver, -} from "./launchable/target-rule"; export { WorkflowTriggerField, WORKFLOW_CATALOG, @@ -23,23 +13,4 @@ export { type WorkflowCatalogEntry, type TriggerFieldsValidation, } from "./catalog"; -export { - ensureRunSession, - recordAgentSessionAtProvision, - endAgentSessionForPrincipal, - endAgentSessionForRun, - type EventCollectorPort, -} from "./launch/agent-session"; -export { - deliverWhenRoutable, - isAgentUnreachableError, - DEFAULT_ROUTABLE_DEADLINE_MS, - type DeliverWhenRoutableOptions, -} from "./deliver-when-routable"; -export { - readFoldedBody, - DefinitionProjectionMissingError, - MultiStepFoldUnsupportedError, - FoldedBodySchema, - type DefinitionCandidate, -} from "./definition-projection"; +export { ensureRunSession, type EventCollectorPort } from "./launch/agent-session"; diff --git a/packages/workflows/src/launch/agent-session.ts b/packages/workflows/src/launch/agent-session.ts index b41d8c3ff7..f1bcf8bf9d 100644 --- a/packages/workflows/src/launch/agent-session.ts +++ b/packages/workflows/src/launch/agent-session.ts @@ -1,5 +1,5 @@ -// Every native launcher writes the `sessionId` it mints here, so mail and -// spans persist from a run's first turn. See docs/agent-session-provisioning.md. +// Upserts a run's `agent_session` so mail and spans persist from its first +// turn. See docs/agent-session-provisioning.md. import { eq } from "drizzle-orm"; import type { DB } from "@intx/db"; import { agentSession, workflowRun, workflowRunLaunchSpec } from "@intx/db/schema"; @@ -9,45 +9,6 @@ import type { EventCollectorRegistry } from "@intx/hub-sessions"; * `EventCollectorRegistry` through, never a second registry construction. */ export type EventCollectorPort = Pick; -/** The one shared write every native launcher calls right after - * `prepareProvisionedDeployment` returns. See - * docs/agent-session-provisioning.md#why-timing-forces-a-two-step-write. */ -export async function recordAgentSessionAtProvision(params: { - readonly db: DB["db"]; - readonly eventCollectors: Pick; - readonly runId: string; - readonly sessionId: string; - readonly sourceAuthorityPrincipalId: string; -}): Promise { - const { db, eventCollectors, runId, sessionId, sourceAuthorityPrincipalId } = params; - const runRow = await db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, runId), - }); - if (runRow === undefined) { - throw new Error( - `recordAgentSessionAtProvision: no workflow_run "${runId}" — prepareProvisionedDeployment must have already returned`, - ); - } - - const now = new Date(); - await db - .insert(agentSession) - .values({ - id: sessionId, - tenantId: runRow.tenantId, - agentId: runRow.definitionId, - principalId: sourceAuthorityPrincipalId, - status: "active", - createdAt: now, - updatedAt: now, - }) - .onConflictDoNothing({ target: agentSession.id }); - - if (runRow.address !== null && !eventCollectors.has(runRow.address)) { - eventCollectors.create(runRow.address, runRow.tenantId, sessionId, runRow.id); - } -} - /** True upsert of a run's `agent_session`, keyed by the run's own principal * once Interchange anchors one. See docs/agent-session-provisioning.md. */ export async function ensureRunSession(params: { @@ -104,36 +65,3 @@ export async function ensureRunSession(params: { } return sessionId; } - -/** Marks a run's `agent_session` ended. No sweeper; a run nobody reacts to - * stays `active` until something does. */ -export async function endAgentSessionForPrincipal( - db: DB["db"], - principalId: string, -): Promise { - const endedAt = new Date(); - await db - .update(agentSession) - .set({ status: "ended", endedAt, updatedAt: endedAt }) - .where(eq(agentSession.principalId, principalId)); -} - -/** Same as `endAgentSessionForPrincipal`, keyed by run id, plus abandons the - * run's event collector. No-ops for a run row already gone or never - * triggered. */ -export async function endAgentSessionForRun( - db: DB["db"], - anchorRunId: string, - eventCollectors: Pick, -): Promise { - const runRow = await db.query.workflowRun.findFirst({ - where: eq(workflowRun.id, anchorRunId), - }); - if (runRow === undefined || runRow.principalId === null) { - return; - } - await endAgentSessionForPrincipal(db, runRow.principalId); - if (runRow.address !== null) { - eventCollectors.abandon(runRow.address); - } -} diff --git a/packages/workflows/src/launchable/target-rule.ts b/packages/workflows/src/launchable/target-rule.ts deleted file mode 100644 index 97cb1d810c..0000000000 --- a/packages/workflows/src/launchable/target-rule.ts +++ /dev/null @@ -1,127 +0,0 @@ -// The follow-latest rule, pure — no `drizzle-orm`, no `@intx/db` — so this -// half is browser-safe and testable without a database. -export type LaunchableDefinitionRejection = - | "not_found" - | "unfrozen" - | "not_deployed" - | "cross_tenant"; - -export type LaunchableDefinitionResolution = - | { - readonly ok: true; - readonly definitionId: string; - readonly wireHash: string; - } - | { readonly ok: false; readonly reason: LaunchableDefinitionRejection }; - -export type LaunchableDefinitionResolver = ( - tenantId: string, - definitionAssetId: string, -) => Promise; - -/** The columns the pick needs from one `workflow_definition` row, declared - * as a plain shape so the ordering rule is testable without a database. */ -export type LaunchableDefinitionCandidate = { - readonly id: string; - readonly tenantId: string; - readonly status: string; - readonly approvedWireHash: string | null; - readonly grantSnapshot: unknown; - readonly wireProjection: unknown; - readonly createdAt: Date; -}; - -/** The one predicate for "did a deploy freeze land," shared by the - * follow-latest rule and the definition-lifecycle derivation. */ -export function isFrozen(candidate: { - readonly approvedWireHash: string | null; - readonly grantSnapshot: unknown; - readonly wireProjection: unknown; -}): boolean { - return ( - candidate.approvedWireHash !== null && - candidate.grantSnapshot !== null && - candidate.grantSnapshot !== undefined && - candidate.wireProjection !== null && - candidate.wireProjection !== undefined - ); -} - -/** The follow-latest rule: the newest row in `tenantId` that is `deployed` - * and frozen wins. The rejection reason is the most specific the rows - * support, so a cross-tenant reference is named as such, not "missing." */ -export function pickLaunchableDefinition( - candidates: readonly LaunchableDefinitionCandidate[], - tenantId: string, -): LaunchableDefinitionResolution { - if (candidates.length === 0) return { ok: false, reason: "not_found" }; - const inTenant = candidates.filter((row) => row.tenantId === tenantId); - if (inTenant.length === 0) return { ok: false, reason: "cross_tenant" }; - const deployed = inTenant.filter((row) => row.status === "deployed"); - if (deployed.length === 0) return { ok: false, reason: "not_deployed" }; - const frozen = deployed.filter(isFrozen); - if (frozen.length === 0) return { ok: false, reason: "unfrozen" }; - // A `createdAt` tie breaks on `id` desc for a deterministic total order. - const newest = [...frozen].sort((a, b) => { - const byCreatedAt = b.createdAt.getTime() - a.createdAt.getTime(); - if (byCreatedAt !== 0) return byCreatedAt; - return a.id < b.id ? 1 : a.id > b.id ? -1 : 0; - })[0]; - if (newest === undefined || newest.approvedWireHash === null) { - return { ok: false, reason: "unfrozen" }; - } - return { - ok: true, - definitionId: newest.id, - wireHash: newest.approvedWireHash, - }; -} - -/** The typed refusal a route answers with when a routine's target does not - * resolve. A cross-tenant asset reports as not found. */ -export function routineTargetRejection(reason: LaunchableDefinitionRejection): { - readonly status: 404 | 409; - readonly code: string; - readonly userMessage: string; -} { - switch (reason) { - case "not_found": - case "cross_tenant": - return { - status: 404, - code: "routine_target_not_found", - userMessage: "No workflow with that id exists in this workspace.", - }; - case "not_deployed": - return { - status: 409, - code: "routine_target_not_deployed", - userMessage: - "That workflow has no deployed version yet — deploy it, then point the routine at it.", - }; - case "unfrozen": - return { - status: 409, - code: "routine_target_not_approved", - userMessage: - "That workflow's deployment has not been approved yet — approve it, then point the routine at it.", - }; - } -} - -/** Thrown by a launcher when a routine fires and its target no longer - * resolves — fails closed rather than running whatever row exists. */ -export class RoutineTargetUnresolvableError extends Error { - readonly reason: LaunchableDefinitionRejection; - readonly definitionAssetId: string; - constructor(definitionAssetId: string, reason: LaunchableDefinitionRejection) { - super( - `routine target ${definitionAssetId} has no launchable definition (${reason}): ${ - routineTargetRejection(reason).userMessage - }`, - ); - this.name = "RoutineTargetUnresolvableError"; - this.reason = reason; - this.definitionAssetId = definitionAssetId; - } -} diff --git a/packages/workflows/src/source.ts b/packages/workflows/src/source.ts index 6f66adf2de..2e854377d2 100644 --- a/packages/workflows/src/source.ts +++ b/packages/workflows/src/source.ts @@ -16,8 +16,6 @@ export const WORKFLOW_SOURCE_DIRECTORS = `./${WORKFLOW_SOURCE_DIRECTORS_PATH}`; export const WORKFLOW_SOURCE_MANIFEST_PATH = "package.json"; /** The JSON projection of the definition, for readers. */ export const WORKFLOW_SOURCE_DEFINITION_PATH = "definition.json"; -/** The path a pre-retirement asset carried its definition at. */ -export const RETIRED_WORKFLOW_ENVELOPE_PATH = "workflow.json"; export type WorkflowSourceTree = Readonly>; From bf3a1f764405d6f2c1d1c3ef08ee211bf1561477 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 11:41:46 -0700 Subject: [PATCH 2/5] chore(approvals): delete the unused grant-allowance gate and its vendored delta note (CL-9641) --- VENDORED.md | 2 +- bun.lock | 21 -- packages/approvals/README.md | 18 +- packages/approvals/package.json | 13 +- packages/approvals/src/allowance.test.ts | 248 ----------------------- packages/approvals/src/allowance.ts | 171 ---------------- packages/approvals/src/index.ts | 13 -- packages/approvals/tsconfig.json | 13 +- packages/approvals/tsconfig.src.json | 13 +- vendor/intx/hub-api/VENDORED-FROM | 2 +- 10 files changed, 7 insertions(+), 507 deletions(-) delete mode 100644 packages/approvals/src/allowance.test.ts delete mode 100644 packages/approvals/src/allowance.ts diff --git a/VENDORED.md b/VENDORED.md index 2e74db3680..e08445d592 100644 --- a/VENDORED.md +++ b/VENDORED.md @@ -32,7 +32,7 @@ Pinned to [faremeter/interchange](https://github.com/faremeter/interchange) | `vendor/intx/db` | `@intx/db`, with one local serving-time credential-refresh delta | maintainers | 2026-11-03 | | `vendor/intx/harness` | `@intx/harness` at a newer commit than npm has published | maintainers | 2026-11-03 | | `vendor/intx/hub-agent` | `@intx/hub-agent`, with a local OAuth-login-over-websocket delta | maintainers | 2026-11-03 | -| `vendor/intx/hub-api` | `@intx/hub-api`, with local approval and workflow-deploy-bearer routes | maintainers | 2026-11-03 | +| `vendor/intx/hub-api` | `@intx/hub-api`, with the local workflow-deploy-bearer route | maintainers | 2026-11-03 | | `vendor/intx/hub-sessions` | `@intx/hub-sessions`, with several local run/pack-acceptance fixes | maintainers | 2026-11-03 | | `vendor/intx/inference` | `@intx/inference`, with one local Google file-upload compile fix | maintainers | 2026-11-03 | | `vendor/intx/mail-memory` | `@intx/mail-memory` at a newer commit than npm has published | maintainers | 2026-11-03 | diff --git a/bun.lock b/bun.lock index f3b89921af..dd51ac891c 100644 --- a/bun.lock +++ b/bun.lock @@ -157,19 +157,8 @@ "packages/approvals": { "name": "@corbits/approvals", "version": "0.0.1", - "dependencies": { - "@intx/authz": "0.4.0", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/types": "workspace:*", - "arktype": "catalog:", - "drizzle-orm": "catalog:", - "hono": "catalog:", - }, "devDependencies": { - "@intx/hub-common": "0.4.0", "@types/bun": "catalog:", - "postgres": "catalog:", "typescript": "catalog:", }, }, @@ -1847,10 +1836,6 @@ "@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], - "@corbits/mcp/@corbits/credential-mcp": ["@corbits/credential-mcp@github:corbitsdev/corbits-credential-mcp#fb9ec3a", { "peerDependencies": { "@intx/harness": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-credential-mcp-fb9ec3a", "sha512-Co4jR+L3pfBbVYY7I6jRftoHeSb5+LCxzEEaAhVLYA3aY2PrPHZV37+DWxj3fcK/KdzboqbT3XSnqU8DyHRU9Q=="], - - "@corbits/worker/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#4a2a3c2", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/agent": "^0.3.0", "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-4a2a3c2", "sha512-UY5nkQ31kGmYgqgIOSlbigwvm7c/s2kpqEbF6Oqo0rzoS1RejpPp+w/Yim6h2MRpYCl5CXgAET1MhkDboHuWBA=="], - "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@npmcli/agent/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], @@ -1867,12 +1852,6 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@workbench/hub/@corbits/mcp": ["@corbits/mcp@github:corbitsdev/corbits-mcp#9df1ab8", { "dependencies": { "@corbits/credential-mcp": "github:corbitsdev/corbits-credential-mcp#fb9ec3a6754348cd33c388c70c0830b8844c83de", "@intx/db": "^0.3.0", "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "arktype": "2.1.20", "drizzle-orm": "^0.45.2", "hono": "^4.11.9" }, "peerDependencies": { "@intx/agent": ">=0.3.0" } }, "corbitsdev-corbits-mcp-9df1ab8", "sha512-5ILvgNI4J/U2kWitFKZmIJO1qybCdIkKZ73ZELu6PWrbw6tARxO8/ffaGoK0nIMrT4EcBZaMd6YkR0vLdPOWnw=="], - - "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#8be608b", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/types": "^0.3.0", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-8be608b", "sha512-7NcVcz3IHFTfx6ICQT+4gjX1PBNLqjlCqoEOPDFVW7wc4e2+Ueg9wOZQ+0MVttpQCaDg6MObl4kie410UQm/sQ=="], - - "@workbench/web/@corbits/credential-mcp": ["@corbits/credential-mcp@github:corbitsdev/corbits-credential-mcp#fb9ec3a", { "peerDependencies": { "@intx/harness": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-credential-mcp-fb9ec3a", "sha512-Co4jR+L3pfBbVYY7I6jRftoHeSb5+LCxzEEaAhVLYA3aY2PrPHZV37+DWxj3fcK/KdzboqbT3XSnqU8DyHRU9Q=="], - "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], diff --git a/packages/approvals/README.md b/packages/approvals/README.md index bb9796df45..cfa71018ab 100644 --- a/packages/approvals/README.md +++ b/packages/approvals/README.md @@ -1,30 +1,16 @@ # @corbits/approvals -Decides when a tenant's standing grants already authorize a tool call, so -an agent is only stopped for a decision a human has not already made — and -names an approval in words a person can act on. This package never creates, +Names an approval in words a person can act on. This package never creates, resolves, or claims anything: listing, reading, approving and rejecting all live on Interchange's own `/api/tenants/:tenantId/approvals` routes, whose authorize + claimTerminal + resolve transaction is already exactly-once and grant-scoped. -## Composition over Interchange - -- The allowance gate evaluates the tenant's own grant rules through - `@intx/authz`'s `evaluateGrants` — no parallel policy engine, and no - approval store or state machine of its own. -- Names and statuses are read straight off Interchange's native approval - and run views by whoever renders them; nothing here mirrors those rows. - ## Key modules -- `allowance.ts` — `createToolAllowanceRegistry`, `evaluateToolAllowance`, - `withGrantAllowance`: the gate that auto-approves a call an existing - grant already covers. - `headline.ts` — `headlineFor`: the pure headline builder, exported at - `@corbits/approvals/headline` for browser callers (the root entry reaches - server-only dependencies). + `@corbits/approvals/headline` for browser callers. - `index.ts` — package entry point. ## Tests diff --git a/packages/approvals/package.json b/packages/approvals/package.json index 3bb800c805..3cdf2e2875 100644 --- a/packages/approvals/package.json +++ b/packages/approvals/package.json @@ -2,7 +2,7 @@ "name": "@corbits/approvals", "version": "0.0.1", "private": true, - "description": "The grant-allowance gate that decides when a tenant's standing grants already authorize a tool call, plus the pure headline builder that names an approval for a human", + "description": "The pure headline builder that names an approval for a human", "license": "LGPL-2.1-only", "type": "module", "exports": { @@ -13,19 +13,8 @@ "typecheck": "tsc --noEmit", "test": "bun test" }, - "dependencies": { - "@intx/authz": "0.4.0", - "@intx/db": "workspace:*", - "@intx/hub-api": "workspace:*", - "@intx/types": "workspace:*", - "arktype": "catalog:", - "drizzle-orm": "catalog:", - "hono": "catalog:" - }, "devDependencies": { - "@intx/hub-common": "0.4.0", "@types/bun": "catalog:", - "postgres": "catalog:", "typescript": "catalog:" } } diff --git a/packages/approvals/src/allowance.test.ts b/packages/approvals/src/allowance.test.ts deleted file mode 100644 index 7c0a28786d..0000000000 --- a/packages/approvals/src/allowance.test.ts +++ /dev/null @@ -1,248 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import type { GrantRule } from "@intx/types/authz"; - -import { - createToolAllowanceRegistry, - evaluateToolAllowance, - withGrantAllowance, - type ToolAllowance, -} from "./allowance"; - -const TENANT = "tenant_1"; - -function repoGrant(overrides?: Partial): GrantRule { - return { - id: "grant_repo", - resource: "repo:acme/rocket", - action: "read", - effect: "allow", - origin: "system", - conditions: null, - expiresAt: null, - roleId: "role_member", - principalId: null, - ...overrides, - }; -} - -const readDiffAllowance: ToolAllowance = { - tool: "read_diff", - grantAction: "read", - classify: (_tenantId, args) => - Promise.resolve( - typeof args["repo"] === "string" - ? { readOnly: true, resource: `repo:${args["repo"]}` } - : { readOnly: false }, - ), -}; - -const mergeAllowance: ToolAllowance = { - tool: "merge_pr", - grantAction: "read", - classify: () => Promise.resolve({ readOnly: false }), -}; - -const registry = createToolAllowanceRegistry([readDiffAllowance, mergeAllowance]); - -describe("createToolAllowanceRegistry", () => { - test("rejects duplicate tool annotations", () => { - expect(() => createToolAllowanceRegistry([readDiffAllowance, readDiffAllowance])).toThrow( - /duplicate allowance/, - ); - }); -}); - -describe("evaluateToolAllowance", () => { - test("read-only call with a covering grant rides, attributed to the grant", async () => { - const decision = await evaluateToolAllowance({ - registry, - tenantId: TENANT, - toolName: "read_diff", - toolArguments: { repo: "acme/rocket" }, - grants: [repoGrant()], - }); - expect(decision).toEqual({ - outcome: "ride", - resource: "repo:acme/rocket", - grantId: "grant_repo", - }); - }); - - test("read-only call without a covering grant parks", async () => { - const decision = await evaluateToolAllowance({ - registry, - tenantId: TENANT, - toolName: "read_diff", - toolArguments: { repo: "acme/other" }, - grants: [repoGrant()], - }); - expect(decision).toEqual({ outcome: "park", reason: "no_covering_grant" }); - }); - - test("write call parks even when the resource grant exists", async () => { - const decision = await evaluateToolAllowance({ - registry, - tenantId: TENANT, - toolName: "merge_pr", - toolArguments: { repo: "acme/rocket" }, - grants: [repoGrant()], - }); - expect(decision).toEqual({ outcome: "park", reason: "not_read_only" }); - }); - - test("unannotated tool parks regardless of grants", async () => { - const decision = await evaluateToolAllowance({ - registry, - tenantId: TENANT, - toolName: "push_branch", - toolArguments: {}, - grants: [repoGrant()], - }); - expect(decision).toEqual({ outcome: "park", reason: "unclassified" }); - }); - - test("a deny grant on the resource never rides", async () => { - const decision = await evaluateToolAllowance({ - registry, - tenantId: TENANT, - toolName: "read_diff", - toolArguments: { repo: "acme/rocket" }, - grants: [repoGrant(), repoGrant({ id: "grant_deny", effect: "deny" })], - }); - expect(decision).toEqual({ outcome: "park", reason: "no_covering_grant" }); - }); - - test("a throwing classifier fails closed", async () => { - const throwing = createToolAllowanceRegistry([ - { - tool: "read_diff", - grantAction: "read", - classify: () => Promise.reject(new Error("server unreachable")), - }, - ]); - const decision = await evaluateToolAllowance({ - registry: throwing, - tenantId: TENANT, - toolName: "read_diff", - toolArguments: { repo: "acme/rocket" }, - grants: [repoGrant()], - }); - expect(decision).toEqual({ - outcome: "park", - reason: "classification_failed", - }); - }); -}); - -describe("withGrantAllowance", () => { - type Registered = Parameters>[0]; - - function gateHarness(overrides?: { grants?: GrantRule[]; autoApproveResult?: boolean }) { - const calls = { - base: [] as Registered[], - autoApprove: [] as { - approvalId: string; - tenantId: string; - resource: string; - grantId: string; - }[], - log: [] as string[], - }; - const gate = withGrantAllowance( - async (args: Registered) => { - calls.base.push(args); - }, - { - registry, - findRegisteredApproval: (correlationId) => - Promise.resolve({ - approvalId: `apr_${correlationId}`, - tenantId: TENANT, - }), - listTenantGrants: () => Promise.resolve(overrides?.grants ?? [repoGrant()]), - autoApprove: (args) => { - calls.autoApprove.push(args); - return Promise.resolve(overrides?.autoApproveResult ?? true); - }, - log: (line) => calls.log.push(line), - }, - ); - return { gate, calls }; - } - - test("read-only call with covering grant auto-approves after registration", async () => { - const { gate, calls } = gateHarness(); - await gate({ - correlationId: "c1", - approvalSnapshot: { - name: "read_diff", - arguments: { repo: "acme/rocket" }, - }, - }); - expect(calls.base).toHaveLength(1); - expect(calls.autoApprove).toEqual([ - { - approvalId: "apr_c1", - tenantId: TENANT, - resource: "repo:acme/rocket", - grantId: "grant_repo", - }, - ]); - expect(calls.log[0]).toContain("auto-approved"); - }); - - test("read-only call without covering grant stays parked", async () => { - const { gate, calls } = gateHarness({ grants: [] }); - await gate({ - correlationId: "c2", - approvalSnapshot: { - name: "read_diff", - arguments: { repo: "acme/rocket" }, - }, - }); - expect(calls.base).toHaveLength(1); - expect(calls.autoApprove).toHaveLength(0); - expect(calls.log[0]).toContain("parked (no_covering_grant)"); - }); - - test("write call with grant stays parked", async () => { - const { gate, calls } = gateHarness(); - await gate({ - correlationId: "c3", - approvalSnapshot: { - name: "merge_pr", - arguments: { repo: "acme/rocket" }, - }, - }); - expect(calls.autoApprove).toHaveLength(0); - expect(calls.log[0]).toContain("parked (not_read_only)"); - }); - - test("unannotated tool takes the fast path: registration only", async () => { - const { gate, calls } = gateHarness(); - await gate({ - correlationId: "c4", - approvalSnapshot: { name: "push_branch", arguments: {} }, - }); - expect(calls.base).toHaveLength(1); - expect(calls.autoApprove).toHaveLength(0); - expect(calls.log).toHaveLength(0); - }); - - test("an allowance failure never breaks registration", async () => { - const gate = withGrantAllowance(async () => {}, { - registry, - findRegisteredApproval: () => Promise.reject(new Error("db down")), - listTenantGrants: () => Promise.resolve([]), - autoApprove: () => Promise.resolve(true), - log: () => {}, - }); - await expect( - gate({ - correlationId: "c5", - approvalSnapshot: { name: "read_diff", arguments: {} }, - }), - ).resolves.toBeUndefined(); - }); -}); diff --git a/packages/approvals/src/allowance.ts b/packages/approvals/src/allowance.ts deleted file mode 100644 index be59efaa02..0000000000 --- a/packages/approvals/src/allowance.ts +++ /dev/null @@ -1,171 +0,0 @@ -// Read-only tool calls ride the grant allowance instead of per-call -// approval: a call whose tool declares an allowance, classifies read-only -// for these arguments, and whose resource an existing grant covers is -// auto-approved through the native resolve machinery — the approval row -// still exists and flips to "approved", so the decision is ledgered, but no -// card needs a human. Everything else keeps today's parked behavior. -import { evaluateGrants } from "@intx/authz"; -import type { GrantRule } from "@intx/types/authz"; - -export type AllowanceClassification = - | { readonly readOnly: true; readonly resource: string } - | { readonly readOnly: false }; - -/** One tool's declarative allowance: `classify` answers whether this call is - * read-only and which resource it touches; `grantAction` names the action a - * covering grant must allow on that resource. */ -export type ToolAllowance = { - /** Qualified tool name exactly as the approval row records it. */ - readonly tool: string; - readonly grantAction: string; - readonly classify: ( - tenantId: string, - toolArguments: Record, - ) => Promise; -}; - -export type ToolAllowanceRegistry = ReadonlyMap; - -export function createToolAllowanceRegistry( - allowances: readonly ToolAllowance[], -): ToolAllowanceRegistry { - const registry = new Map(); - for (const allowance of allowances) { - if (registry.has(allowance.tool)) { - throw new Error( - `createToolAllowanceRegistry: duplicate allowance for tool "${allowance.tool}"`, - ); - } - registry.set(allowance.tool, allowance); - } - return registry; -} - -export type AllowanceDecision = - | { - readonly outcome: "ride"; - readonly resource: string; - readonly grantId: string; - } - | { - readonly outcome: "park"; - readonly reason: - | "unclassified" - | "not_read_only" - | "no_resource" - | "no_covering_grant" - | "classification_failed"; - }; - -/** The pure allowance decision. Fails closed — a throwing classifier parks - * rather than rides. */ -export async function evaluateToolAllowance(args: { - registry: ToolAllowanceRegistry; - tenantId: string; - toolName: string; - toolArguments: Record; - grants: GrantRule[]; -}): Promise { - const allowance = args.registry.get(args.toolName); - if (allowance === undefined) return { outcome: "park", reason: "unclassified" }; - - let classification: AllowanceClassification; - try { - classification = await allowance.classify(args.tenantId, args.toolArguments); - } catch { - return { outcome: "park", reason: "classification_failed" }; - } - if (!classification.readOnly) { - return { outcome: "park", reason: "not_read_only" }; - } - if (classification.resource.length === 0) { - return { outcome: "park", reason: "no_resource" }; - } - - const decision = await evaluateGrants( - args.grants, - classification.resource, - allowance.grantAction, - { tenantId: args.tenantId }, - ); - if (decision.effect !== "allow" || decision.resolvedBy === null) { - return { outcome: "park", reason: "no_covering_grant" }; - } - return { - outcome: "ride", - resource: classification.resource, - grantId: decision.resolvedBy.id, - }; -} - -export type RegisteredApprovalRef = { - readonly approvalId: string; - readonly tenantId: string; -}; - -export type GrantAllowanceGateDeps = { - registry: ToolAllowanceRegistry; - /** The pending approval the registration just co-wrote, by correlation. */ - findRegisteredApproval(correlationId: string): Promise; - /** The tenant's live grant rows — role- and principal-scoped alike. */ - listTenantGrants(tenantId: string): Promise; - /** Resolves the approval "approved" with allowance (null-principal) - * authority; a false is logged, never thrown. */ - autoApprove(args: { - approvalId: string; - tenantId: string; - resource: string; - grantId: string; - }): Promise; - log(line: string): void; -}; - -type RegisterApprovalArgs = { - readonly correlationId: string; - readonly approvalSnapshot: { - readonly name: string; - readonly arguments: Record; - }; -}; - -/** Wraps a registration to evaluate the allowance and auto-approve a riding - * call. Never throws into the registration path. */ -export function withGrantAllowance( - base: (args: Args) => Promise, - deps: GrantAllowanceGateDeps, -): (args: Args) => Promise { - return async (args: Args) => { - await base(args); - if (!deps.registry.has(args.approvalSnapshot.name)) return; - try { - const registered = await deps.findRegisteredApproval(args.correlationId); - if (registered === null) return; - const decision = await evaluateToolAllowance({ - registry: deps.registry, - tenantId: registered.tenantId, - toolName: args.approvalSnapshot.name, - toolArguments: args.approvalSnapshot.arguments, - grants: await deps.listTenantGrants(registered.tenantId), - }); - if (decision.outcome !== "ride") { - deps.log(`grant-allowance: ${args.approvalSnapshot.name} parked (${decision.reason})`); - return; - } - const resolved = await deps.autoApprove({ - approvalId: registered.approvalId, - tenantId: registered.tenantId, - resource: decision.resource, - grantId: decision.grantId, - }); - deps.log( - resolved - ? `grant-allowance: ${args.approvalSnapshot.name} rode grant ${decision.grantId} on ${decision.resource} (approval ${registered.approvalId} auto-approved)` - : `grant-allowance: ${args.approvalSnapshot.name} covered by ${decision.grantId} but approval ${registered.approvalId} did not resolve; left parked`, - ); - } catch (cause) { - deps.log( - `grant-allowance: ${args.approvalSnapshot.name} evaluation failed, left parked: ${cause instanceof Error ? cause.message : String(cause)}`, - ); - } - }; -} diff --git a/packages/approvals/src/index.ts b/packages/approvals/src/index.ts index bc2d71c4fd..9c88734a95 100644 --- a/packages/approvals/src/index.ts +++ b/packages/approvals/src/index.ts @@ -1,14 +1 @@ -export { - createToolAllowanceRegistry, - evaluateToolAllowance, - withGrantAllowance, -} from "./allowance"; -export type { - AllowanceClassification, - AllowanceDecision, - GrantAllowanceGateDeps, - RegisteredApprovalRef, - ToolAllowance, - ToolAllowanceRegistry, -} from "./allowance"; export { argumentsSummaryFor, headlineFor, toolNameFor } from "./headline"; diff --git a/packages/approvals/tsconfig.json b/packages/approvals/tsconfig.json index 90fd46501c..e125e90614 100644 --- a/packages/approvals/tsconfig.json +++ b/packages/approvals/tsconfig.json @@ -11,16 +11,5 @@ "rootDir": "../.." }, "include": ["src"], - "exclude": [], - "references": [ - { - "path": "../../vendor/intx/db/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/hub-api/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/types/tsconfig.src.json" - } - ] + "exclude": [] } diff --git a/packages/approvals/tsconfig.src.json b/packages/approvals/tsconfig.src.json index 641d835700..fc8724305f 100644 --- a/packages/approvals/tsconfig.src.json +++ b/packages/approvals/tsconfig.src.json @@ -8,16 +8,5 @@ "emitDeclarationOnly": true, "outDir": "dist", "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - }, - "references": [ - { - "path": "../../vendor/intx/db/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/hub-api/tsconfig.src.json" - }, - { - "path": "../../vendor/intx/types/tsconfig.src.json" - } - ] + } } diff --git a/vendor/intx/hub-api/VENDORED-FROM b/vendor/intx/hub-api/VENDORED-FROM index 1b0deed77b..5d6dc27d19 100644 --- a/vendor/intx/hub-api/VENDORED-FROM +++ b/vendor/intx/hub-api/VENDORED-FROM @@ -1,4 +1,4 @@ Source: https://github.com/faremeter/interchange (packages/hub-api) Commit: 5453d0b01da9d749aa446c4ba34fcf2aeb4549c0 (origin/main, v0.4.0) License: LGPL-2.1-only (see vendor/intx/LICENSE) -Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. Upstream test files are not vendored (they import the unvendored @intx/inference-testing and @intx/test-harness test packages); only the tests covering a workbench delta are kept. CL-6345: resolveApproval's ResolveApprovalRequest accepts principalId: null for policy-resolved (grant-allowance) decisions, which skip the per-principal approval:/resolve gate the HTTP routes still enforce; readDurableWorkflowRunLifecycles is re-exported from the barrel for the hub's grant-allowance gate. CL-workflow-deploy-bearer: middleware/workflow-run-deploy-auth.ts (+ test) is a bearer-authenticated mirror onto the session-cookie POST/GET .../workflows/deployments route; MountHubRoutesDeps and CreateAppOpts gain an optional workflowRunAuthenticator that mounts it ahead of resolveTenant, and src/index.ts exports the middleware and its types. +Local modifications: exports map repointed from the upstream intx-src condition to direct TypeScript source resolution (types/default -> ./src/...); dist references removed. Upstream test files are not vendored (they import the unvendored @intx/inference-testing and @intx/test-harness test packages); only the tests covering a workbench delta are kept. CL-workflow-deploy-bearer: middleware/workflow-run-deploy-auth.ts (+ test) is a bearer-authenticated mirror onto the session-cookie POST/GET .../workflows/deployments route; MountHubRoutesDeps and CreateAppOpts gain an optional workflowRunAuthenticator that mounts it ahead of resolveTenant, and src/index.ts exports the middleware and its types. From 4d6793389c9de3faf93a1df159793d0e637ecdbe Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 11:44:18 -0700 Subject: [PATCH 3/5] chore(web): delete the workbench-to-workspace MCP catalog migration shim (CL-9641) --- apps/web/src/mcp-servers.ts | 59 ++----------------------------------- 1 file changed, 2 insertions(+), 57 deletions(-) diff --git a/apps/web/src/mcp-servers.ts b/apps/web/src/mcp-servers.ts index 32e7f6aa25..06ccf7b64c 100644 --- a/apps/web/src/mcp-servers.ts +++ b/apps/web/src/mcp-servers.ts @@ -8,7 +8,6 @@ // workbench Worker binds it by walking up, so two workbenches share one Exa // row and a second workspace sees none of it. import { MCP_NO_TOKEN_SENTINEL, MCP_STREAMABLE_HTTP_PROVIDER_KEY } from "@corbits/credential-mcp"; -import { reportError } from "@corbits/error-sink"; import { EXA_MCP_SERVER, MCP_SERVER_CATALOG, @@ -344,52 +343,6 @@ export async function removeMcpServer( }); } -/** Legacy workbench-scoped MCP rows predate the workspace catalog: before it, - * every workbench stored its own Exa. Moves the keyless (`auth === "none"`) - * rows up to the workspace — their sentinel secret is reconstructible, so - * re-adding rediscovers without touching a real credential — deduped by - * handle, then drops the workbench rows they came from. A token row carries - * a secret the client may never read, so it stays where it is: orphaned but - * untouched (accepted — the next redeploy binds the workspace catalog and a - * leftover row simply stops taking effect). */ -export async function migrateWorkbenchMcpCatalogToWorkspace( - workbenchTenantId: string, - workspaceTenantId: string, - fetchImpl: typeof fetch = fetch, -): Promise { - if (workbenchTenantId === workspaceTenantId) return; - const [workbenchServers, workspaceServers] = await Promise.all([ - listMcpServers(workbenchTenantId, fetchImpl), - listMcpServers(workspaceTenantId, fetchImpl), - ]); - const workspaceHandles = new Set(workspaceServers.map((server) => server.handle)); - for (const server of workbenchServers) { - // Only a keyless row can move without its secret; a token row stays - // orphaned on the workbench (see above). - if (server.auth !== "none") continue; - if (!workspaceHandles.has(server.handle)) { - const moved = await addMcpServer( - { - tenantId: workspaceTenantId, - handle: server.handle, - name: server.name, - url: server.url, - }, - fetchImpl, - ); - workspaceHandles.add(moved.handle); - } - await removeMcpServer( - { - tenantId: workbenchTenantId, - credentialId: server.credentialId, - providerId: server.providerId, - }, - fetchImpl, - ); - } -} - /** Ensures that land on one workspace (two workbench Workers deploying, a * StrictMode double-invoke) run one after another, so a later one sees the * Exa an earlier one added instead of adding its own. */ @@ -399,15 +352,13 @@ const ensureQueue = new Map>(); * the workspace (top-level) tenant, then left alone so a later removal is * not undone by the next start. Callers pass any tenant id — a workbench id * resolves up to its workspace — and each workbench Worker binds the shared - * row by walking up. Also moves that caller's legacy workbench rows up, - * best-effort: a failed cleanup must not block deploying Worker with the - * workspace catalog that is already correct. */ + * row by walking up. */ export async function ensureBuiltInMcpServers( tenantId: string, fetchImpl: typeof fetch = fetch, ): Promise { const workspaceTenantId = await resolveWorkspaceTenantId(tenantId, fetchImpl); - const run = () => runEnsureBuiltInMcpServers(tenantId, workspaceTenantId, fetchImpl); + const run = () => runEnsureBuiltInMcpServers(workspaceTenantId, fetchImpl); const previous = ensureQueue.get(workspaceTenantId); // A failed earlier ensure already rejected to its own caller; this one runs regardless. const flight = previous === undefined ? run() : previous.then(run, run); @@ -420,15 +371,9 @@ export async function ensureBuiltInMcpServers( } async function runEnsureBuiltInMcpServers( - tenantId: string, workspaceTenantId: string, fetchImpl: typeof fetch, ): Promise { - try { - await migrateWorkbenchMcpCatalogToWorkspace(tenantId, workspaceTenantId, fetchImpl); - } catch (cause) { - reportError(cause, { operation: "mcp_servers_catalog_migration" }); - } const existing = await listMcpServers(workspaceTenantId, fetchImpl); if (existing.some((server) => server.handle === EXA_MCP_SERVER.handle)) return existing; const added = await addMcpServer( From 68388f347aaafd8205334ed02ae50ddddd5e3eac Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 11:44:47 -0700 Subject: [PATCH 4/5] fix(url-path): report a malformed path segment through reportError (CL-9641) --- bun.lock | 3 +++ packages/url-path/package.json | 3 +++ packages/url-path/src/decoded-or-null.ts | 5 ++++- packages/url-path/tsconfig.json | 7 ++++++- packages/url-path/tsconfig.src.json | 7 ++++++- 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index dd51ac891c..8a0d97dae2 100644 --- a/bun.lock +++ b/bun.lock @@ -198,6 +198,9 @@ "packages/url-path": { "name": "@corbits/url-path", "version": "0.0.1", + "dependencies": { + "@corbits/error-sink": "workspace:*", + }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:", diff --git a/packages/url-path/package.json b/packages/url-path/package.json index 0fba2e4598..d1925b9dbf 100644 --- a/packages/url-path/package.json +++ b/packages/url-path/package.json @@ -12,6 +12,9 @@ "typecheck": "tsc --noEmit", "test": "bun test" }, + "dependencies": { + "@corbits/error-sink": "workspace:*" + }, "devDependencies": { "@types/bun": "catalog:", "typescript": "catalog:" diff --git a/packages/url-path/src/decoded-or-null.ts b/packages/url-path/src/decoded-or-null.ts index f69de3ec5e..685d4e4288 100644 --- a/packages/url-path/src/decoded-or-null.ts +++ b/packages/url-path/src/decoded-or-null.ts @@ -1,9 +1,12 @@ +import { reportError } from "@corbits/error-sink"; + /** Decodes a URL path segment, reading a malformed one as no selection * rather than throwing. */ export function decodedOrNull(segment: string): string | null { try { return decodeURIComponent(segment); - } catch { + } catch (cause) { + reportError(cause, { operation: "url_path_decode" }); return null; } } diff --git a/packages/url-path/tsconfig.json b/packages/url-path/tsconfig.json index e125e90614..58e347560b 100644 --- a/packages/url-path/tsconfig.json +++ b/packages/url-path/tsconfig.json @@ -11,5 +11,10 @@ "rootDir": "../.." }, "include": ["src"], - "exclude": [] + "exclude": [], + "references": [ + { + "path": "../error-sink/tsconfig.src.json" + } + ] } diff --git a/packages/url-path/tsconfig.src.json b/packages/url-path/tsconfig.src.json index fc8724305f..e7aad06af1 100644 --- a/packages/url-path/tsconfig.src.json +++ b/packages/url-path/tsconfig.src.json @@ -8,5 +8,10 @@ "emitDeclarationOnly": true, "outDir": "dist", "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" - } + }, + "references": [ + { + "path": "../error-sink/tsconfig.src.json" + } + ] } From 3809b8b4fab20a811fc382e5a21ed05b38cbe102 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 11:45:15 -0700 Subject: [PATCH 5/5] chore: drop unused hub deps and list agent-token among the hub mounts (CL-9641) --- AGENTS.md | 2 +- apps/hub/package.json | 2 -- bun.lock | 26 ++++++++++++++++++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 48a043e32d..6eed3651cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ Workbench is a client of [Interchange](https://github.com/faremeter/interchange). The hub mounts only stock `@intx/hub-api` routes plus a set of Corbits libraries, each mounted once (`@corbits/mailbox`, `@corbits/memory`, -`@corbits/artifacts`, `@corbits/webhooks`, `@corbits/cron`, `@corbits/mcp`, an +`@corbits/artifacts`, `@corbits/webhooks`, `@corbits/cron`, `@corbits/mcp`, `@corbits/agent-token`, an OAuth library on `@corbits/oauth-core`); any other hub mount is cutover debt with a Linear issue, never a pattern to extend. A workbench is a plain Interchange tenant, nothing more. The web client sets itself up over stock diff --git a/apps/hub/package.json b/apps/hub/package.json index 3f36699df6..4eb2474bc6 100644 --- a/apps/hub/package.json +++ b/apps/hub/package.json @@ -14,7 +14,6 @@ "dependencies": { "@chat-adapter/state-memory": "^4.35.0", "@corbits/agent-token": "github:corbitsdev/corbits-agent-token#ed286f4592190f911de480ef5330ce03d8605f00", - "@corbits/approvals": "workspace:*", "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#4a2a3c22187991a6ce598faf794908fedd252e98", "@corbits/codex-provider": "github:corbitsdev/corbits-codex-provider#a1f475f341261c05fa8dade0c82f1246c175b5b3", "@corbits/cron": "0.2.1", @@ -23,7 +22,6 @@ "@corbits/mcp": "github:corbitsdev/corbits-mcp#9df1ab81d2f50d798bca2d2f44eb4759c92fac0e", "@corbits/memory": "github:corbitsdev/corbits-memory#8be608becc749fa0795a899826277823dca6db3e", "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core#3028f8604155eee63f5a1711f0d4022d7984e3f7", - "@corbits/url-path": "workspace:*", "@corbits/webhooks": "github:corbitsdev/webhooks#3f4f83147fd826d3aec5bcac7ef4b0ae7f75c00d", "@corbits/workflows": "workspace:*", "@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider#9f2d4bac40ea075df092fef807404a13c638cf14", diff --git a/bun.lock b/bun.lock index 8a0d97dae2..f5574d54cf 100644 --- a/bun.lock +++ b/bun.lock @@ -42,7 +42,6 @@ "dependencies": { "@chat-adapter/state-memory": "^4.35.0", "@corbits/agent-token": "github:corbitsdev/corbits-agent-token#ed286f4592190f911de480ef5330ce03d8605f00", - "@corbits/approvals": "workspace:*", "@corbits/artifacts": "github:corbitsdev/corbits-artifacts#4a2a3c22187991a6ce598faf794908fedd252e98", "@corbits/codex-provider": "github:corbitsdev/corbits-codex-provider#a1f475f341261c05fa8dade0c82f1246c175b5b3", "@corbits/cron": "0.2.1", @@ -51,7 +50,6 @@ "@corbits/mcp": "github:corbitsdev/corbits-mcp#9df1ab81d2f50d798bca2d2f44eb4759c92fac0e", "@corbits/memory": "github:corbitsdev/corbits-memory#8be608becc749fa0795a899826277823dca6db3e", "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core#3028f8604155eee63f5a1711f0d4022d7984e3f7", - "@corbits/url-path": "workspace:*", "@corbits/webhooks": "github:corbitsdev/webhooks#3f4f83147fd826d3aec5bcac7ef4b0ae7f75c00d", "@corbits/workflows": "workspace:*", "@corbits/xai-provider": "github:corbitsdev/corbits-xai-provider#9f2d4bac40ea075df092fef807404a13c638cf14", @@ -1837,8 +1835,12 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@corbits/codex-provider/@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#3028f86", { "dependencies": { "@intx/db": "^0.3.0", "@intx/hub-api": "^0.3.0", "@intx/hub-common": "^0.3.0", "@intx/types": "^0.3.0", "arktype": "2.2.3", "drizzle-orm": "^0.45.2", "hono": "^4.11.9" } }, "corbitsdev-corbits-oauth-core-3028f86", "sha512-KMxRZgEi7S99tqoWhXqAAm5Bv4ef5tn3eYFcfbiHHX/E+GPnbedF44/ISbGs2gedoR2U9EtA1gnmh+q+Qfg00w=="], + "@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], + "@corbits/xai-provider/@corbits/oauth-core": ["@corbits/oauth-core@github:corbitsdev/corbits-oauth-core#3028f86", { "dependencies": { "@intx/db": "^0.3.0", "@intx/hub-api": "^0.3.0", "@intx/hub-common": "^0.3.0", "@intx/types": "^0.3.0", "arktype": "2.2.3", "drizzle-orm": "^0.45.2", "hono": "^4.11.9" } }, "corbitsdev-corbits-oauth-core-3028f86", "sha512-KMxRZgEi7S99tqoWhXqAAm5Bv4ef5tn3eYFcfbiHHX/E+GPnbedF44/ISbGs2gedoR2U9EtA1gnmh+q+Qfg00w=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], "@npmcli/agent/lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], @@ -1855,6 +1857,18 @@ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#4a2a3c2", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/agent": "^0.3.0", "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-4a2a3c2", "sha512-UY5nkQ31kGmYgqgIOSlbigwvm7c/s2kpqEbF6Oqo0rzoS1RejpPp+w/Yim6h2MRpYCl5CXgAET1MhkDboHuWBA=="], + + "@workbench/hub/@corbits/codex-provider": ["@corbits/codex-provider@github:corbitsdev/corbits-codex-provider#a1f475f", { "dependencies": { "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core", "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-codex-provider-a1f475f", "sha512-D6d0lblWx/4KudfLwQoQGrLB8GKRLP/HVp1cx08L2/hOGZAgH6txLPpeEtBqjl+R520qXSSPfM0kDC+3XNaAfw=="], + + "@workbench/hub/@corbits/mailbox": ["@corbits/mailbox@github:corbitsdev/corbits-mailbox#65590a8", { "dependencies": { "@hono/standard-validator": "0.2.3", "@intx/crypto": "0.3.0", "@standard-community/standard-json": "0.3.5", "@standard-community/standard-openapi": "0.2.9", "arktype": "2.1.29", "hono-openapi": "1.3.1" }, "peerDependencies": { "@intx/log": "^0.2.2", "drizzle-orm": "^0.45.2", "hono": "^4.12.0", "postgres": "^3.4.0" } }, "corbitsdev-corbits-mailbox-65590a8", "sha512-tK7xXcL780iX3I+5R68gmyqgEA0uwYO1qdTDNn/LF6tOiFIm0lt2szjspJ8JMKMTzKC6SSai03p723rEGM1jOg=="], + + "@workbench/hub/@corbits/mcp": ["@corbits/mcp@github:corbitsdev/corbits-mcp#9df1ab8", { "dependencies": { "@corbits/credential-mcp": "github:corbitsdev/corbits-credential-mcp#fb9ec3a6754348cd33c388c70c0830b8844c83de", "@intx/db": "^0.3.0", "@intx/hub-api": "^0.3.0", "@intx/types": "^0.3.0", "arktype": "2.1.20", "drizzle-orm": "^0.45.2", "hono": "^4.11.9" }, "peerDependencies": { "@intx/agent": ">=0.3.0" } }, "corbitsdev-corbits-mcp-9df1ab8", "sha512-5ILvgNI4J/U2kWitFKZmIJO1qybCdIkKZ73ZELu6PWrbw6tARxO8/ffaGoK0nIMrT4EcBZaMd6YkR0vLdPOWnw=="], + + "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#8be608b", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/types": "^0.3.0", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-8be608b", "sha512-7NcVcz3IHFTfx6ICQT+4gjX1PBNLqjlCqoEOPDFVW7wc4e2+Ueg9wOZQ+0MVttpQCaDg6MObl4kie410UQm/sQ=="], + + "@workbench/hub/@corbits/xai-provider": ["@corbits/xai-provider@github:corbitsdev/corbits-xai-provider#9f2d4ba", { "dependencies": { "@corbits/oauth-core": "github:corbitsdev/corbits-oauth-core", "@corbits/openai-responses": "github:corbitsdev/corbits-openai-responses", "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-xai-provider-9f2d4ba", "sha512-ZjvKxGcp+QzCh8zeXHg7kNbtkpR/itZkmQAcdG1aOAOjHaNwfGVpkPc/c/XtwPNsQyaDjF8W9CHb4rH1xI2uCA=="], + "better-call/@better-auth/utils": ["@better-auth/utils@0.5.0", "", { "dependencies": { "@noble/hashes": "^2.0.1" } }, "sha512-BL8W4EfIZFwlu0r54m3v1ztjDhu6dDe/amLTm0xybmbZaNgYUqhD3SjpAsnq0q8YD6/ki4iwIgxJNLP/N3TxiA=="], "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], @@ -1949,6 +1963,14 @@ "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@workbench/hub/@corbits/codex-provider/@corbits/openai-responses": ["@corbits/openai-responses@github:corbitsdev/corbits-openai-responses#7efe000", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-openai-responses-7efe000", "sha512-pH6C8T1WDJjqM+YbnQpofd7HgP5krEj48JCCQK16KI98a6kaz+gffpgdOdQpU3aNVZnFlyADXod1Z4tqUG/3ww=="], + + "@workbench/hub/@corbits/mailbox/hono-openapi": ["hono-openapi@1.3.1", "", { "peerDependencies": { "@hono/standard-validator": "^0.2.0", "@standard-community/standard-json": "^0.3.5", "@standard-community/standard-openapi": "^0.2.9", "@types/json-schema": "^7.0.15", "hono": "^4.11.2", "openapi-types": "^12.1.3" }, "optionalPeers": ["@hono/standard-validator", "hono"] }, "sha512-NLVeVkhKZ3drmQNEIPac8HX8Y54uf1hJAgIM/7MfDsaeVVmB+QILWQxx5x3R3NvRHgedcbEbOCGY2uR7WQYyMw=="], + + "@workbench/hub/@corbits/mcp/@corbits/credential-mcp": ["@corbits/credential-mcp@github:corbitsdev/corbits-credential-mcp#fb9ec3a", { "peerDependencies": { "@intx/harness": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-credential-mcp-fb9ec3a", "sha512-Co4jR+L3pfBbVYY7I6jRftoHeSb5+LCxzEEaAhVLYA3aY2PrPHZV37+DWxj3fcK/KdzboqbT3XSnqU8DyHRU9Q=="], + + "@workbench/hub/@corbits/xai-provider/@corbits/openai-responses": ["@corbits/openai-responses@github:corbitsdev/corbits-openai-responses#7efe000", { "dependencies": { "arktype": "2.2.3" }, "peerDependencies": { "@intx/inference": ">=0.3.0", "@intx/types": ">=0.3.0" } }, "corbitsdev-corbits-openai-responses-7efe000", "sha512-pH6C8T1WDJjqM+YbnQpofd7HgP5krEj48JCCQK16KI98a6kaz+gffpgdOdQpU3aNVZnFlyADXod1Z4tqUG/3ww=="], + "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],