Ctrl K
ComunidadGuías…

src/app.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { GATEWAY_HTTP_PATH, handleHttpDeliveryRequest } from './slack/gateway/http-delivery.ts';
import { createAgentSlackAppRoutes } from './slack/agent-apps/index.ts';
import { deploymentTenancy } from './config/installation-scope.ts';
import { tagStateStub } from './config/state-rpc.ts';
import { instrument, type FlueObservation } from '@flue/runtime';
import { Hono, type Context } from 'hono';

import { createAdminRoutes } from './admin/routes.ts';
import { CHICKPEA_SLACK_AGENT_NAME } from './agents/names.ts';
import {
  getConfigStore,
  getIdentityStore,
  getSettingsStore,
  getSlackStateStore,
  type PlatformEnv,
} from './config/state-backend.ts';
import { isCloudflareTarget } from './config/runtime-target.ts';
import {
  isOAuthContinuationActorActive,
  repairPendingOAuthContinuationResumes,
} from './connections/oauth-continuation.ts';
import type { OAuthContinuation } from './connections/types.ts';
import { BETTER_AUTH_PUBLIC_PATHS, MCP_OAUTH_PATHS } from './auth/public-auth-paths.ts';
import { lazyRoutes } from './http/lazy-routes.ts';
import { recoveryOnlyGate } from './auth/request-auth-control.ts';
import { createManagementSetupRoutes } from './management/setup-routes.ts';
import { createConnectRoutes } from './management/connect-routes.ts';
import { activityStatusForObservation } from './activity/status.ts';
import { noteAgentAuthoringActivation } from './management/workspace-change-tools.ts';
import {
  observeProviderAuthRoute,
  providerAuthRouteInterceptor,
} from './audit/provider-auth.ts';
import {
  memoryToolPolicyInterceptor,
  observeMemoryToolPolicy,
} from './memory/tool-policy.ts';
import { publishActivityStatus } from './slack/activity-publisher.ts';
import {
  agentFailureDiagnosticsInterceptor,
  observeAgentResultDiagnostics,
} from './slack/agent-failure-diagnostics.ts';
import { slackToolBoundaryFoldInterceptor } from './slack/follow-up-fold.ts';
import {
  observePresentationToolPolicy,
  presentationToolPolicyInterceptor,
} from './slack/presentation-tool-policy.ts';
import { startNodeTurnRelay, wakeNodeTurnRelay } from './slack/node-turn-relay.ts';
import { createPlatformProductTelemetry } from './telemetry/platform.ts';
import { createRequestTelemetryLifecycle } from './telemetry/runtime.ts';
import { startNodeGatewaySession } from './slack/gateway/node-runtime.ts';
import { currentFlueObservationContext, workModelInvocationInterceptor } from './work/model-invocation.ts';
import { ModelStepMarks } from './observability/runtime-latency.ts';
import { readStagedSlackObservationTarget } from './agents/turn-input.ts';
import { modelAccessInterceptor } from './agents/model-access-lookup.ts';
import { runFeeInterceptor } from './config/run-fee-interceptor.ts';
import { observeModelAccess } from './config/model-access.ts';
import { workspaceRegistryInterceptor } from './sandbox/workspace-registry.ts';
import {
  observeResponseMetadata,
  responseMetadataInterceptor,
} from './usage/response-metadata.ts';
import { admitSetupWake, answerSlackRecoveryEventsProof, channel, serveAgentAppSlackDelivery } from './channels/slack.ts';
import { SLACK_EVENTS_PATH } from './slack/app-manifest.ts';
import {
  bootstrapRuntimeProviders,
  WORKERS_AI_CONTEXT_WINDOW_FLOOR,
} from './runtime-bootstrap.ts';

export { WORKERS_AI_CONTEXT_WINDOW_FLOOR };

// Install the same app-owned Pi providers used by direct agent execution.
// Cloudflare adds its keyless Workers AI binding in the Worker entry.
bootstrapRuntimeProviders();

instrument({
  key: Symbol.for('chickpea.agent-failure-diagnostics'),
  interceptor: agentFailureDiagnosticsInterceptor,
  observe: observeAgentResultDiagnostics,
  dispose() {},
});

// Bridge only safe activity summaries. The work interceptor below restores
// app-owned TurnJob correlation from Flue's instance/submission coordinates;
// no synthetic request header or model-visible attribute carries it.
instrument({
  key: Symbol.for('chickpea.activity-status'),
  interceptor: async (_operation, _context, next) => next(),
  observe(event, context) {
    if (context.agentName !== CHICKPEA_SLACK_AGENT_NAME) return;
    const status = activityStatusForObservation(event);
    if (
      status &&
      typeof event.instanceId === 'string' &&
      typeof event.submissionId === 'string'
    ) {
      publishActivityStatus(event.instanceId, status, context.env, event.submissionId);
    }
  },
  dispose() {},
});

// A Slack thread mounts the workspace change tools on the render after its
// model activates agent-authoring.
instrument({
  key: Symbol.for('chickpea.agent-authoring-activation'),
  interceptor: async (_operation, _context, next) => next(),
  observe(event, context) {
    if (context.agentName === CHICKPEA_SLACK_AGENT_NAME) noteAgentAuthoringActivation(event);
  },
  dispose() {},
});

// Response metadata is the one durable usage-of-record consumed by both the
// interactive Slack relay and routines. It contains token counts and bounded
// model identifiers only — never prompts, completions, credentials, or tool
// arguments.
instrument({
  key: Symbol.for('chickpea.response-metadata'),
  interceptor: responseMetadataInterceptor,
  observe: observeResponseMetadata,
  dispose() {},
});

instrument({
  key: Symbol.for('chickpea.memory-tool-policy'),
  interceptor: memoryToolPolicyInterceptor,
  observe: observeMemoryToolPolicy,
  dispose() {},
});

// One coding-workspace registry per managed submission, shared by the
// workspace tools.
instrument({
  key: Symbol.for('chickpea.workspace-registry'),
  interceptor: workspaceRegistryInterceptor,
  observe() {},
  dispose() {},
});

instrument({
  key: Symbol.for('chickpea.slack-presentation-tool-policy'),
  interceptor: presentationToolPolicyInterceptor,
  observe: observePresentationToolPolicy,
  dispose() {},
});

// Registered after the presentation policy, so it runs inside it: a tool
// that policy refuses never counts as a tool boundary.
instrument({
  key: Symbol.for('chickpea.slack-tool-boundary-fold'),
  interceptor: slackToolBoundaryFoldInterceptor,
  observe() {},
  dispose() {},
});

// Flue emits one turn_request for every main, structured-output, retry, and
// compaction model operation. Its provider id is already credential-free; add
// the exact product route fact to the same trace without prompts, account data,
// tokens, or billing guesses.
instrument({
  key: Symbol.for('chickpea.provider-auth-route'),
  interceptor: providerAuthRouteInterceptor,
  observe: observeProviderAuthRoute,
  dispose() {},
});

// Canonical invocation state changes at Flue's first model operation, after
// agent initialization and live policy resolution but before provider work.
instrument({
  key: Symbol.for('chickpea.work-model-invocation'),
  interceptor: workModelInvocationInterceptor,
  observe() {},
  dispose() {},
});

// Every attempt of every agent binds its run's model access before its first
// model call; the provider proxy refuses a call without it. Registered inside
// the work interceptor, so a Slack attempt reuses its TurnJob match.
instrument({
  key: Symbol.for('chickpea.model-access'),
  interceptor: modelAccessInterceptor,
  observe: observeModelAccess,
  dispose() {},
});

// One content-free model_step record per model request, timed in the object
// that makes it; a Slack turn's names the turn.
const modelSteps = new ModelStepMarks();
instrument({
  key: Symbol.for('chickpea.model-step-latency'),
  interceptor: async (_operation, _context, next) => next(),
  observe(event, context) {
    modelSteps.observe(event, () => (context.agentName === CHICKPEA_SLACK_AGENT_NAME ? slackTurnJobIdOf(event) : undefined));
  },
  dispose() {},
});

/** The Slack turn a thread agent's activity belongs to: the attempt's own match, else its staged input. */
function slackTurnJobIdOf({ instanceId, submissionId }: FlueObservation): string | undefined {
  if (!instanceId || !submissionId) return undefined;
  const active = currentFlueObservationContext();
  if (active?.target && active.instanceId === instanceId && active.submissionId === submissionId) {
    return active.target.turnJobId;
  }
  return readStagedSlackObservationTarget(instanceId, submissionId)?.turnJobId;
}

instrument({
  key: Symbol.for('chickpea.run-fees'),
  interceptor: runFeeInterceptor,
  observe() {},
  dispose() {},
});

const app = new Hono();
app.post(GATEWAY_HTTP_PATH, (c) => {
  // No installation of a deployment serving many takes gateway deliveries.
  if (!isCloudflareTarget() || deploymentTenancy(c.env as PlatformEnv | undefined) === 'installation') return c.notFound();
  return handleHttpDeliveryRequest(c.req.raw, input => tagStateStub(c.env as PlatformEnv).receiveGatewayHttp(input));
});
// Recovery-only closes everything but Slack credential recovery and the paths
// with their own bearer capability; the Events URL answers only recovery's
// signed proof. Admin mounts the same gate; it and authentication reuse this read.
app.use('*', recoveryOnlyGate(
  (c) => getIdentityStore(c.env as PlatformEnv | undefined),
  new Map([[SLACK_EVENTS_PATH, answerSlackRecoveryEventsProof]]),
));
// Starts the shared startup/periodic wake for durable compatibility TurnJobs
// and ledger-authoritative interactive Runs. Ledger admission stays default-off
// and exact-channel scoped by SLACK_TAG_LEDGER_CANARY_CHANNELS.
startNodeTurnRelay();
startNodeGatewaySession();

async function resumeOAuthContinuation(
  continuation: OAuthContinuation,
  env?: PlatformEnv,
): Promise<void> {
  if (!(await isOAuthContinuationActorActive({
    continuation,
    identity: getIdentityStore(env),
  }))) {
    throw new Error('The Slack member who authorized this connection is no longer active.');
  }
  const resumed = await getSlackStateStore(env).resumeTurnAfterOAuth?.(
    continuation.taskId,
    continuation.id,
  );
  if (!resumed) throw new Error('The interrupted Slack task is no longer available.');
  await wakeNodeTurnRelay(env);
}

async function repairOAuthResumes(env?: PlatformEnv): Promise<void> {
  await repairPendingOAuthContinuationResumes({
    settings: getSettingsStore(env),
    onReady: (continuation) => resumeOAuthContinuation(continuation, env),
  });
}

function productTelemetryForRequest(c: Context) {
  const platformEnv = c.env as PlatformEnv | undefined;
  return createPlatformProductTelemetry({
    ...(platformEnv ? { env: platformEnv } : {}),
    settings: getSettingsStore(platformEnv),
    config: getConfigStore(platformEnv),
    lifecycle: createRequestTelemetryLifecycle(c),
  });
}

if (!isCloudflareTarget()) {
  let oauthRepairInFlight: Promise<void> | undefined;
  const runOAuthRepair = () => {
    if (oauthRepairInFlight) return oauthRepairInFlight;
    oauthRepairInFlight = repairOAuthResumes()
      .catch(() => undefined)
      .finally(() => {
        oauthRepairInFlight = undefined;
      });
    return oauthRepairInFlight;
  };
  queueMicrotask(() => void runOAuthRepair());
  const oauthRepairTimer = setInterval(
    () => void runOAuthRepair(),
    60_000,
  );
  oauthRepairTimer.unref();
}
// Better Auth and the MCP resource load on their first request; a Slack turn
// never needs them, and their modules are about 1.5 MiB of the static closure.
app.route('/', lazyRoutes('Better Auth routes', BETTER_AUTH_PUBLIC_PATHS, async () =>
  (await import('./auth/better-auth-runtime.ts')).createBetterAuthRuntimeRoutes()));
app.route('/', lazyRoutes('MCP OAuth routes', MCP_OAUTH_PATHS, async () =>
  (await import('./auth/mcp-oauth-routes.ts')).createMcpOAuthRuntimeRoutes({ productTelemetry: productTelemetryForRequest })));
app.route('/', createManagementSetupRoutes({
  productTelemetry: productTelemetryForRequest,
  wakeAgent: admitSetupWake,
}));
app.route('/', createConnectRoutes());
app.route('/', createAdminRoutes({
  onOAuthContinuationReady: resumeOAuthContinuation,
  productTelemetry: productTelemetryForRequest,
}));
app.route('/', createAgentSlackAppRoutes({ serveDelivery: serveAgentAppSlackDelivery }));
app.route('/channels/slack', channel.route());

export default app;