From d7b1661e1b732205ee426b6cb18b3d8a855bc3fa Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Mon, 10 Aug 2026 21:48:01 -0700 Subject: [PATCH] improvement(skills): align application operation guidance --- .agents/skills/add-integration/SKILL.md | 6 +- .../migrate-application-operation/SKILL.md | 37 +++++++---- .../agents/openai.yaml | 6 +- .claude/commands/add-integration.md | 6 +- .../commands/migrate-application-operation.md | 37 +++++++---- .claude/rules/global.md | 7 +- .claude/rules/sim-architecture.md | 15 +++++ .cursor/commands/add-integration.md | 6 +- .../commands/migrate-application-operation.md | 35 ++++++---- .cursor/rules/global.mdc | 7 +- .cursor/rules/sim-architecture.mdc | 15 +++++ AGENTS.md | 65 ++++++++++--------- CLAUDE.md | 65 ++++++++++--------- apps/sim/AGENTS.md | 56 ++++++++++------ 14 files changed, 234 insertions(+), 129 deletions(-) diff --git a/.agents/skills/add-integration/SKILL.md b/.agents/skills/add-integration/SKILL.md index e72dd878bbe..3369828d6f2 100644 --- a/.agents/skills/add-integration/SKILL.md +++ b/.agents/skills/add-integration/SKILL.md @@ -768,9 +768,11 @@ tools: { } ``` -#### 3. Create Internal API Route +#### 3. Create Special Internal Tool Execution Route -Create `apps/sim/app/api/tools/{service}/{action}/route.ts`. Internal tool routes are HTTP boundaries and follow the same contract policy as public routes — define the request/response shape in `apps/sim/lib/api/contracts/tools/{service}.ts` (or an existing aggregate) and validate with canonical helpers from `@/lib/api/server`. Never write a route-local Zod schema. +Create `apps/sim/app/api/tools/{service}/{action}/route.ts`. This raw route pattern is only for an integration's provider-execution boundary when it needs special file normalization, large-body handling, or protocol behavior. It is not the pattern for CRUD or other operations on protected Sim resources. For those, use the `migrate-application-operation` skill and an authorized application use case with the ordinary internal/v2 route builders. + +Internal tool routes are HTTP boundaries and follow the same contract policy as public routes — define the request/response shape in `apps/sim/lib/api/contracts/tools/{service}.ts` (or an existing aggregate) and validate with canonical helpers from `@/lib/api/server`. Never write a route-local Zod schema. Authenticate and perform cheap admission before parsing or downloading files. ```typescript // apps/sim/lib/api/contracts/tools/{service}.ts diff --git a/.agents/skills/migrate-application-operation/SKILL.md b/.agents/skills/migrate-application-operation/SKILL.md index fa7923be6a7..e7f0039e84b 100644 --- a/.agents/skills/migrate-application-operation/SKILL.md +++ b/.agents/skills/migrate-application-operation/SKILL.md @@ -1,11 +1,11 @@ --- name: migrate-application-operation -description: Migrate one existing Sim resource operation into the shared Principal and application-use-case architecture across internal APIs, public or versioned APIs, Copilot, and other trusted tool adapters. Use when removing route- or tool-local authorization and business logic, consolidating resource reads or writes behind semantic operation policies, or adding another surface to an existing application operation while preserving contracts, identity, errors, rate limits, audit, analytics, and compatibility behavior. Treat v1, uploads, streams, large bodies, bulk recursion, and polymorphic tools as explicitly scoped special cases. +description: Create or migrate a protected Sim resource operation in the shared Principal and application-use-case architecture across internal APIs, public or versioned APIs, Copilot, and other trusted tool adapters. Use when adding a protected endpoint, tool command, or CRUD method; removing route- or tool-local authorization and business logic; consolidating resource reads or writes behind semantic operation policies; or adding another surface to an existing application operation while preserving contracts, identity, errors, rate limits, audit, analytics, and compatibility behavior. Treat v1, uploads, streams, large bodies, bulk recursion, and polymorphic tools as explicitly scoped special cases. --- -# Migrate Application Operation +# Create Or Migrate Application Operation -Migrate one bounded semantic operation at a time. Share authorization and business behavior without forcing internal APIs, public APIs, Copilot, and other tools to share authentication, input schemas, or response shapes. +Create or migrate one bounded semantic operation at a time. Share authorization and business behavior without forcing internal APIs, public APIs, Copilot, and other tools to share authentication, input schemas, or response shapes. ## Enforce the application boundary @@ -43,8 +43,12 @@ Read these files completely before editing: - `apps/sim/lib/core/application/workspace-operation.ts` - `apps/sim/lib/core/application/workspace-authorization.ts` - `apps/sim/lib/core/application/authorized-workspace-use-case.ts` +- `apps/sim/lib/api/server/routes/definition.ts` - `apps/sim/lib/api/server/routes/internal-json-route.ts` - `apps/sim/lib/api/server/routes/v2-json-route.ts` +- `apps/sim/lib/auth/internal-delegation.ts` +- `apps/sim/lib/copilot/application/application-adapter.ts` +- `apps/sim/lib/copilot/auth/application-delegation.ts` Use the file domain only as a representative golden slice: @@ -116,10 +120,11 @@ rename: defineWorkspaceOperation({ minimumRole: 'write', workspaceApiKey: 'allow', principalKinds: ['session', 'personal_api_key', 'workspace_api_key', 'delegated'], + delegatedServices: ['copilot'], }) ``` -Do not create internal-, public-, or Copilot-specific versions of the same semantic operation. If two callers have materially different business or transactional semantics, define separate use cases and explain the distinction. +Do not create internal-, public-, or Copilot-specific versions of the same semantic operation. If two callers have materially different business or transactional semantics, define separate semantic operations and use cases and explain the distinction. Choose principal kinds from actual behavior. Do not accept every principal merely because the use case is shared. Workspace API keys have a write ceiling and cannot satisfy admin operations. The operation definition must fail fast when its role, workspace-key policy, and principal kinds disagree. @@ -165,17 +170,21 @@ Do not call shared authorization, principal audit attribution, or `recordAudit` Inspect legacy orchestration before reusing it. If it already authorizes, audits, notifies, or captures analytics, call a lower-level primitive or remove duplicate responsibility for migrated callers. +Application code must remain surface-neutral. It must not import `app/api/**`, `next/server`, internal/v1/v2 contracts or presenters, or Copilot tool handlers. Return domain values and let each surface presenter project its own wire result. + ## Adapt internal APIs -Use `defineInternalJsonRoute` for ordinary JSON routes. Explicitly declare the contract, session authentication policy, semantic operation, rate policy, error policy, input mapping, use case, and presenter when the wire result differs. +Use `defineInternalJsonRoute` for ordinary JSON routes. Explicitly declare the contract, authentication policy, semantic operation, rate policy, error policy, input mapping, use case, and presenter when the wire result differs. + +Use `internalSessionAuth` for session-only routes. Use `createInternalSessionOrExecutorAuth` only when the endpoint genuinely supports signed executor delegation; the semantic operation must then allow `delegated` principals from the `executor` service. Never turn an actorless legacy JWT into a fake session, owner, or user principal. -The internal adapter owns session authentication and internal response envelopes. It must not implement workspace authorization. Preserve internal-only analytics through `onSuccess` after application success. +The internal adapter owns authentication and internal response envelopes. It must not implement workspace authorization. Preserve internal-only analytics through `onSuccess` after application success. Keep the route module declarative. If several internal routes repeat authentication, parsing, error rendering, or response construction, improve the shared internal route builder instead of adding a domain-specific route wrapper. ## Adapt public or versioned APIs -Use the appropriate public/versioned route builder, such as `defineV2JsonRoute`, with API-key authentication, explicit operation rate policy, rollout policy, external error projection, and an external presenter. +Use the appropriate public/versioned route builder, such as `defineV2JsonRoute`, with API-key authentication, explicit semantic operation and rate policy, external error projection, input mapping, application use case, and an external presenter. V2 rollout admission is centralized by the builder; do not invent a route-local rollout policy. Authentication and HTTP formatting may differ from internal APIs; authorization and business behavior must not. Rate-limit using the credential or principal subject, never a billed owner. Resolve billing attribution only for billing, quota, or legacy required-user fields. @@ -185,7 +194,9 @@ Keep v1 middleware and routes unchanged unless explicitly included. ## Adapt Copilot -Create one domain-level Copilot application adapter instead of constructing delegated principals in every tool: +Copilot is a surface adapter, not a separate application layer. If an HTTP or other surface already uses an application use case, Copilot must call that exact use case rather than reimplementing protected business behavior under `lib/copilot`. + +Create one domain-level Copilot application adapter with `createCopilotApplicationAdapter` instead of constructing delegated principals in every tool: ```ts executeCopilotWidgetUseCase(context, renameWidget, input, { resourceId }) @@ -198,15 +209,16 @@ That adapter must: - Construct the shared delegated `Principal` in one place. - Optionally bind the canonical resource scope after trusted resolution. - Verify that the use case exposes a registered code-defined operation. +- Use the domain's exact immutable operation registry so operation-object membership and identity are checked centrally. - Call the application use case directly. Never construct authoritative delegation from model-provided workspace IDs, user IDs, operation IDs, resource scope, or permission tags. Model arguments are requested targets only and must be checked against trusted execution context and canonical data. -Tool handlers own argument aliases, resumable legacy names, abort checks, and tool-specific presentation. They must not query managers directly for protected operations or manually authorize. +Tool handlers own argument aliases, resumable legacy names, abort checks, tool-call reporting, and tool-specific presentation. They must not query managers directly for protected operations, manually authorize, or implement protected business behavior. If a Copilot-only compound action expresses real domain behavior, define a surface-neutral domain operation and application use case for it. -A Copilot reference helper may translate a path to a resource only by calling an authorized application resolver under the intended semantic operation. Passing a code-defined operation object is acceptable; passing a model-provided operation string is not. Reauthorizing during later execution is safe but redundant. When resolution and execution form one business operation, need a consistent snapshot, or appear repeatedly together, prefer a top-level application use case such as `renameWidgetByReference`. +A Copilot reference helper may translate a path to a resource only by calling an authorized application resolver under the intended semantic operation. Passing a code-defined operation object is acceptable; passing a model-provided operation string is not. An immediate same-request resolver followed by the operation may reuse one trusted principal, though the application operation still performs its own canonical authorization. Fresh authentication and authorization are required across lifecycle boundaries such as resumed tool calls, executor callbacks, queued or background work, upload control legs and finalization, durable completion, and long-running provider operations. When resolution and execution form one business operation, need a consistent snapshot, or appear repeatedly together, prefer a top-level application use case such as `renameWidgetByReference`. -Special composition roots may resolve one principal and deliberately thread it through several application calls or lower-level admission stages. Keep this exceptional and explicit; ordinary tools should use the shared execution adapter. +Surface adapters must not compose protected mutations. An atomic compound action requires one top-level semantic domain operation and application use case that owns the transaction and authoritative result. An explicitly best-effort application command may coordinate multiple operations only when it defines hard input and expansion caps, cancellation checkpoints, partial-result semantics, audit behavior, and rate/quota policy. Keep composition exceptional and explicit; ordinary tools should use the shared execution adapter. Map expected typed errors to safe tool results. Unknown errors must become generic system/retryable messages while retaining full causes in server logs. Never return raw database or storage errors to the model. @@ -252,10 +264,11 @@ Stop and report a missing design rather than weakening identity, authorization, Add focused tests for every migrated surface and principal kind allowed by the operation: - Application: allowed and disallowed roles, principal-kind rejection before canonical loading, workspace assertion mismatch, delegated scope, not found, conflict, no-op, and infrastructure propagation. +- Operation registry: role/workspace-key/principal-kind/delegated-service consistency and fail-fast rejection of invalid definitions. - Repository: canonical active lookup, workspace-predicated writes, archived resources, authoritative affected rows, and database error propagation. - Internal API: authentication before parsing, exact contract, typed errors, and surface analytics only after success. - Public API: personal and workspace keys, rate and rollout behavior, concealment, exact external envelope, and rate headers. -- Copilot or tools: trusted context, rejected forged scope, aliases and resume paths, permission re-check, safe errors, and unchanged tool result shapes. +- Copilot or tools: trusted context, exact registered operation membership, rejected forged scope, aliases and resume paths, permission re-check, safe errors, and unchanged tool result shapes. - Side effects: audit derives from authoritative results; shared notifications follow audit; neither occurs for rejection or no-op. Run at minimum: diff --git a/.agents/skills/migrate-application-operation/agents/openai.yaml b/.agents/skills/migrate-application-operation/agents/openai.yaml index 625ce6954ca..2efab38a8af 100644 --- a/.agents/skills/migrate-application-operation/agents/openai.yaml +++ b/.agents/skills/migrate-application-operation/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "Migrate Application Operation" - short_description: "Share one operation across API and tool surfaces" - default_prompt: "Use $migrate-application-operation to migrate one resource operation across internal APIs, public APIs, Copilot, and other tools." + display_name: "Create Or Migrate Application Operation" + short_description: "Share one protected operation across surfaces" + default_prompt: "Use $migrate-application-operation to create or migrate one protected resource operation across internal APIs, public APIs, Copilot, and other tools." diff --git a/.claude/commands/add-integration.md b/.claude/commands/add-integration.md index 864dc9ab9b3..08d8fc92f08 100644 --- a/.claude/commands/add-integration.md +++ b/.claude/commands/add-integration.md @@ -767,9 +767,11 @@ tools: { } ``` -#### 3. Create Internal API Route +#### 3. Create Special Internal Tool Execution Route -Create `apps/sim/app/api/tools/{service}/{action}/route.ts`. Internal tool routes are HTTP boundaries and follow the same contract policy as public routes — define the request/response shape in `apps/sim/lib/api/contracts/tools/{service}.ts` (or an existing aggregate) and validate with canonical helpers from `@/lib/api/server`. Never write a route-local Zod schema. +Create `apps/sim/app/api/tools/{service}/{action}/route.ts`. This raw route pattern is only for an integration's provider-execution boundary when it needs special file normalization, large-body handling, or protocol behavior. It is not the pattern for CRUD or other operations on protected Sim resources. For those, use the `migrate-application-operation` skill and an authorized application use case with the ordinary internal/v2 route builders. + +Internal tool routes are HTTP boundaries and follow the same contract policy as public routes — define the request/response shape in `apps/sim/lib/api/contracts/tools/{service}.ts` (or an existing aggregate) and validate with canonical helpers from `@/lib/api/server`. Never write a route-local Zod schema. Authenticate and perform cheap admission before parsing or downloading files. ```typescript // apps/sim/lib/api/contracts/tools/{service}.ts diff --git a/.claude/commands/migrate-application-operation.md b/.claude/commands/migrate-application-operation.md index 6411a56f3ad..bc61333c358 100644 --- a/.claude/commands/migrate-application-operation.md +++ b/.claude/commands/migrate-application-operation.md @@ -1,10 +1,10 @@ --- -description: Migrate one existing Sim resource operation into the shared Principal and application-use-case architecture across internal APIs, public or versioned APIs, Copilot, and other trusted tool adapters. Use when removing route- or tool-local authorization and business logic, consolidating resource reads or writes behind semantic operation policies, or adding another surface to an existing application operation while preserving contracts, identity, errors, rate limits, audit, analytics, and compatibility behavior. Treat v1, uploads, streams, large bodies, bulk recursion, and polymorphic tools as explicitly scoped special cases. +description: Create or migrate a protected Sim resource operation in the shared Principal and application-use-case architecture across internal APIs, public or versioned APIs, Copilot, and other trusted tool adapters. Use when adding a protected endpoint, tool command, or CRUD method; removing route- or tool-local authorization and business logic; consolidating resource reads or writes behind semantic operation policies; or adding another surface to an existing application operation while preserving contracts, identity, errors, rate limits, audit, analytics, and compatibility behavior. Treat v1, uploads, streams, large bodies, bulk recursion, and polymorphic tools as explicitly scoped special cases. --- -# Migrate Application Operation +# Create Or Migrate Application Operation -Migrate one bounded semantic operation at a time. Share authorization and business behavior without forcing internal APIs, public APIs, Copilot, and other tools to share authentication, input schemas, or response shapes. +Create or migrate one bounded semantic operation at a time. Share authorization and business behavior without forcing internal APIs, public APIs, Copilot, and other tools to share authentication, input schemas, or response shapes. ## Enforce the application boundary @@ -42,8 +42,12 @@ Read these files completely before editing: - `apps/sim/lib/core/application/workspace-operation.ts` - `apps/sim/lib/core/application/workspace-authorization.ts` - `apps/sim/lib/core/application/authorized-workspace-use-case.ts` +- `apps/sim/lib/api/server/routes/definition.ts` - `apps/sim/lib/api/server/routes/internal-json-route.ts` - `apps/sim/lib/api/server/routes/v2-json-route.ts` +- `apps/sim/lib/auth/internal-delegation.ts` +- `apps/sim/lib/copilot/application/application-adapter.ts` +- `apps/sim/lib/copilot/auth/application-delegation.ts` Use the file domain only as a representative golden slice: @@ -115,10 +119,11 @@ rename: defineWorkspaceOperation({ minimumRole: 'write', workspaceApiKey: 'allow', principalKinds: ['session', 'personal_api_key', 'workspace_api_key', 'delegated'], + delegatedServices: ['copilot'], }) ``` -Do not create internal-, public-, or Copilot-specific versions of the same semantic operation. If two callers have materially different business or transactional semantics, define separate use cases and explain the distinction. +Do not create internal-, public-, or Copilot-specific versions of the same semantic operation. If two callers have materially different business or transactional semantics, define separate semantic operations and use cases and explain the distinction. Choose principal kinds from actual behavior. Do not accept every principal merely because the use case is shared. Workspace API keys have a write ceiling and cannot satisfy admin operations. The operation definition must fail fast when its role, workspace-key policy, and principal kinds disagree. @@ -164,17 +169,21 @@ Do not call shared authorization, principal audit attribution, or `recordAudit` Inspect legacy orchestration before reusing it. If it already authorizes, audits, notifies, or captures analytics, call a lower-level primitive or remove duplicate responsibility for migrated callers. +Application code must remain surface-neutral. It must not import `app/api/**`, `next/server`, internal/v1/v2 contracts or presenters, or Copilot tool handlers. Return domain values and let each surface presenter project its own wire result. + ## Adapt internal APIs -Use `defineInternalJsonRoute` for ordinary JSON routes. Explicitly declare the contract, session authentication policy, semantic operation, rate policy, error policy, input mapping, use case, and presenter when the wire result differs. +Use `defineInternalJsonRoute` for ordinary JSON routes. Explicitly declare the contract, authentication policy, semantic operation, rate policy, error policy, input mapping, use case, and presenter when the wire result differs. + +Use `internalSessionAuth` for session-only routes. Use `createInternalSessionOrExecutorAuth` only when the endpoint genuinely supports signed executor delegation; the semantic operation must then allow `delegated` principals from the `executor` service. Never turn an actorless legacy JWT into a fake session, owner, or user principal. -The internal adapter owns session authentication and internal response envelopes. It must not implement workspace authorization. Preserve internal-only analytics through `onSuccess` after application success. +The internal adapter owns authentication and internal response envelopes. It must not implement workspace authorization. Preserve internal-only analytics through `onSuccess` after application success. Keep the route module declarative. If several internal routes repeat authentication, parsing, error rendering, or response construction, improve the shared internal route builder instead of adding a domain-specific route wrapper. ## Adapt public or versioned APIs -Use the appropriate public/versioned route builder, such as `defineV2JsonRoute`, with API-key authentication, explicit operation rate policy, rollout policy, external error projection, and an external presenter. +Use the appropriate public/versioned route builder, such as `defineV2JsonRoute`, with API-key authentication, explicit semantic operation and rate policy, external error projection, input mapping, application use case, and an external presenter. V2 rollout admission is centralized by the builder; do not invent a route-local rollout policy. Authentication and HTTP formatting may differ from internal APIs; authorization and business behavior must not. Rate-limit using the credential or principal subject, never a billed owner. Resolve billing attribution only for billing, quota, or legacy required-user fields. @@ -184,7 +193,9 @@ Keep v1 middleware and routes unchanged unless explicitly included. ## Adapt Copilot -Create one domain-level Copilot application adapter instead of constructing delegated principals in every tool: +Copilot is a surface adapter, not a separate application layer. If an HTTP or other surface already uses an application use case, Copilot must call that exact use case rather than reimplementing protected business behavior under `lib/copilot`. + +Create one domain-level Copilot application adapter with `createCopilotApplicationAdapter` instead of constructing delegated principals in every tool: ```ts executeCopilotWidgetUseCase(context, renameWidget, input, { resourceId }) @@ -197,15 +208,16 @@ That adapter must: - Construct the shared delegated `Principal` in one place. - Optionally bind the canonical resource scope after trusted resolution. - Verify that the use case exposes a registered code-defined operation. +- Use the domain's exact immutable operation registry so operation-object membership and identity are checked centrally. - Call the application use case directly. Never construct authoritative delegation from model-provided workspace IDs, user IDs, operation IDs, resource scope, or permission tags. Model arguments are requested targets only and must be checked against trusted execution context and canonical data. -Tool handlers own argument aliases, resumable legacy names, abort checks, and tool-specific presentation. They must not query managers directly for protected operations or manually authorize. +Tool handlers own argument aliases, resumable legacy names, abort checks, tool-call reporting, and tool-specific presentation. They must not query managers directly for protected operations, manually authorize, or implement protected business behavior. If a Copilot-only compound action expresses real domain behavior, define a surface-neutral domain operation and application use case for it. -A Copilot reference helper may translate a path to a resource only by calling an authorized application resolver under the intended semantic operation. Passing a code-defined operation object is acceptable; passing a model-provided operation string is not. Reauthorizing during later execution is safe but redundant. When resolution and execution form one business operation, need a consistent snapshot, or appear repeatedly together, prefer a top-level application use case such as `renameWidgetByReference`. +A Copilot reference helper may translate a path to a resource only by calling an authorized application resolver under the intended semantic operation. Passing a code-defined operation object is acceptable; passing a model-provided operation string is not. An immediate same-request resolver followed by the operation may reuse one trusted principal, though the application operation still performs its own canonical authorization. Fresh authentication and authorization are required across lifecycle boundaries such as resumed tool calls, executor callbacks, queued or background work, upload control legs and finalization, durable completion, and long-running provider operations. When resolution and execution form one business operation, need a consistent snapshot, or appear repeatedly together, prefer a top-level application use case such as `renameWidgetByReference`. -Special composition roots may resolve one principal and deliberately thread it through several application calls or lower-level admission stages. Keep this exceptional and explicit; ordinary tools should use the shared execution adapter. +Surface adapters must not compose protected mutations. An atomic compound action requires one top-level semantic domain operation and application use case that owns the transaction and authoritative result. An explicitly best-effort application command may coordinate multiple operations only when it defines hard input and expansion caps, cancellation checkpoints, partial-result semantics, audit behavior, and rate/quota policy. Keep composition exceptional and explicit; ordinary tools should use the shared execution adapter. Map expected typed errors to safe tool results. Unknown errors must become generic system/retryable messages while retaining full causes in server logs. Never return raw database or storage errors to the model. @@ -251,10 +263,11 @@ Stop and report a missing design rather than weakening identity, authorization, Add focused tests for every migrated surface and principal kind allowed by the operation: - Application: allowed and disallowed roles, principal-kind rejection before canonical loading, workspace assertion mismatch, delegated scope, not found, conflict, no-op, and infrastructure propagation. +- Operation registry: role/workspace-key/principal-kind/delegated-service consistency and fail-fast rejection of invalid definitions. - Repository: canonical active lookup, workspace-predicated writes, archived resources, authoritative affected rows, and database error propagation. - Internal API: authentication before parsing, exact contract, typed errors, and surface analytics only after success. - Public API: personal and workspace keys, rate and rollout behavior, concealment, exact external envelope, and rate headers. -- Copilot or tools: trusted context, rejected forged scope, aliases and resume paths, permission re-check, safe errors, and unchanged tool result shapes. +- Copilot or tools: trusted context, exact registered operation membership, rejected forged scope, aliases and resume paths, permission re-check, safe errors, and unchanged tool result shapes. - Side effects: audit derives from authoritative results; shared notifications follow audit; neither occurs for rejection or no-op. Run at minimum: diff --git a/.claude/rules/global.md b/.claude/rules/global.md index 86b2ee3be27..afd2290e37d 100644 --- a/.claude/rules/global.md +++ b/.claude/rules/global.md @@ -4,7 +4,12 @@ Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID. ## API Route Handlers -All API route handlers must be wrapped with `withRouteHandler` from `@/lib/core/utils/with-route-handler`. Never export a bare `async function GET/POST/...` — always use `export const METHOD = withRouteHandler(...)`. +All API route handlers must run inside `withRouteHandler`. Ordinary internal and v2 JSON/binary handlers use the shared route builders, which already apply it; never double-wrap them. Use raw `withRouteHandler` only for documented protocol or lifecycle exceptions. Never export a bare `async function GET/POST/...`. + +## Application Operation Boundary +Every protected read, write, canonical lookup, or authorization-sensitive reference resolution must enter through an authorized application use case. Surfaces authenticate and build a `Principal`, rate-limit, parse, map input, call the use case, and present their own result. They must not query protected data, decide resource authorization, implement business transactions, or record semantic audit. + +Define one stable semantic operation with its role, workspace-key policy, principal kinds, and delegated services. Internal, v2, Copilot, and trusted-tool adapters call the same use case when domain behavior is the same. Copilot uses `createCopilotApplicationAdapter`; it is not a separate protected business layer. Protected compound mutations require one top-level semantic application operation. Never substitute billing attribution, an uploader, creator, or key owner for the acting principal. Use the `migrate-application-operation` skill for new or migrated protected operations. ## Comments Use TSDoc for documentation. No `====` separators. No non-TSDoc comments. diff --git a/.claude/rules/sim-architecture.md b/.claude/rules/sim-architecture.md index a0cfbfcd050..6886710a61c 100644 --- a/.claude/rules/sim-architecture.md +++ b/.claude/rules/sim-architecture.md @@ -38,6 +38,21 @@ packages/ # @sim/* — audit, auth, db, logger, realtime-protocol - `apps/* → packages/*` only. Packages never import from `apps/*`. - `apps/realtime` avoids Next.js, React, the block/tool registry, provider SDKs, and the executor; never add `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` imports to any package it consumes. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`. +## Protected Application Operations + +Every real operation on protected or persisted data crosses one authorized application boundary: + +1. The surface authenticates its credential or trusted context and constructs a `Principal`. +2. A fixed, code-defined semantic operation declares minimum role, workspace-key policy, allowed principal kinds, and delegated services. +3. The application use case loads canonical context, checks asserted scope, authorizes current access, executes the manager/repository, projects semantic audit, and runs shared domain effects. +4. The surface presents its own internal, v2, Copilot, or tool result. + +Routes and tools must not query protected data, authorize resources, implement business transactions, or record semantic audit. Application modules must not import `app/api/**`, `next/server`, route contracts/presenters, or Copilot handlers. Copilot must call the same domain use case through `createCopilotApplicationAdapter`; do not create a second Copilot business implementation. Atomic compound mutations need one top-level semantic application operation rather than sequential surface calls. + +Ordinary internal and v2 routes use the shared JSON/binary route builders. Those builders already apply `withRouteHandler`; do not double-wrap them. Use raw `withRouteHandler` only for explicit protocol, streaming, large-body, multipart, or lifecycle exceptions, while keeping protected business work inside application use cases. + +Use the `migrate-application-operation` skill before creating or migrating a protected endpoint, tool command, or resource method. + ## The `'use client'` server boundary Every export of a `'use client'` module becomes a *client reference* on the server — server-evaluated code (RSC pages/layouts, `prefetch.ts`, route handlers, block definitions, triggers) can only *render* it as a component or pass it as a prop, never *call* it (doing so throws at runtime, e.g. `tableKeys.list is not a function`; `next build` does not catch it). Keep server-importable query primitives (key factories, fetchers, mappers, constants) in non-`'use client'` modules — see `.claude/rules/sim-queries.md`. Enforced by `scripts/check-client-boundary-imports.ts`. diff --git a/.cursor/commands/add-integration.md b/.cursor/commands/add-integration.md index 40cc28d8b8f..193707613f7 100644 --- a/.cursor/commands/add-integration.md +++ b/.cursor/commands/add-integration.md @@ -762,9 +762,11 @@ tools: { } ``` -#### 3. Create Internal API Route +#### 3. Create Special Internal Tool Execution Route -Create `apps/sim/app/api/tools/{service}/{action}/route.ts`. Internal tool routes are HTTP boundaries and follow the same contract policy as public routes — define the request/response shape in `apps/sim/lib/api/contracts/tools/{service}.ts` (or an existing aggregate) and validate with canonical helpers from `@/lib/api/server`. Never write a route-local Zod schema. +Create `apps/sim/app/api/tools/{service}/{action}/route.ts`. This raw route pattern is only for an integration's provider-execution boundary when it needs special file normalization, large-body handling, or protocol behavior. It is not the pattern for CRUD or other operations on protected Sim resources. For those, use the `migrate-application-operation` skill and an authorized application use case with the ordinary internal/v2 route builders. + +Internal tool routes are HTTP boundaries and follow the same contract policy as public routes — define the request/response shape in `apps/sim/lib/api/contracts/tools/{service}.ts` (or an existing aggregate) and validate with canonical helpers from `@/lib/api/server`. Never write a route-local Zod schema. Authenticate and perform cheap admission before parsing or downloading files. ```typescript // apps/sim/lib/api/contracts/tools/{service}.ts diff --git a/.cursor/commands/migrate-application-operation.md b/.cursor/commands/migrate-application-operation.md index f8c67ba42cd..9fac674ca6f 100644 --- a/.cursor/commands/migrate-application-operation.md +++ b/.cursor/commands/migrate-application-operation.md @@ -1,6 +1,6 @@ -# Migrate Application Operation +# Create Or Migrate Application Operation -Migrate one bounded semantic operation at a time. Share authorization and business behavior without forcing internal APIs, public APIs, Copilot, and other tools to share authentication, input schemas, or response shapes. +Create or migrate one bounded semantic operation at a time. Share authorization and business behavior without forcing internal APIs, public APIs, Copilot, and other tools to share authentication, input schemas, or response shapes. ## Enforce the application boundary @@ -38,8 +38,12 @@ Read these files completely before editing: - `apps/sim/lib/core/application/workspace-operation.ts` - `apps/sim/lib/core/application/workspace-authorization.ts` - `apps/sim/lib/core/application/authorized-workspace-use-case.ts` +- `apps/sim/lib/api/server/routes/definition.ts` - `apps/sim/lib/api/server/routes/internal-json-route.ts` - `apps/sim/lib/api/server/routes/v2-json-route.ts` +- `apps/sim/lib/auth/internal-delegation.ts` +- `apps/sim/lib/copilot/application/application-adapter.ts` +- `apps/sim/lib/copilot/auth/application-delegation.ts` Use the file domain only as a representative golden slice: @@ -111,10 +115,11 @@ rename: defineWorkspaceOperation({ minimumRole: 'write', workspaceApiKey: 'allow', principalKinds: ['session', 'personal_api_key', 'workspace_api_key', 'delegated'], + delegatedServices: ['copilot'], }) ``` -Do not create internal-, public-, or Copilot-specific versions of the same semantic operation. If two callers have materially different business or transactional semantics, define separate use cases and explain the distinction. +Do not create internal-, public-, or Copilot-specific versions of the same semantic operation. If two callers have materially different business or transactional semantics, define separate semantic operations and use cases and explain the distinction. Choose principal kinds from actual behavior. Do not accept every principal merely because the use case is shared. Workspace API keys have a write ceiling and cannot satisfy admin operations. The operation definition must fail fast when its role, workspace-key policy, and principal kinds disagree. @@ -160,17 +165,21 @@ Do not call shared authorization, principal audit attribution, or `recordAudit` Inspect legacy orchestration before reusing it. If it already authorizes, audits, notifies, or captures analytics, call a lower-level primitive or remove duplicate responsibility for migrated callers. +Application code must remain surface-neutral. It must not import `app/api/**`, `next/server`, internal/v1/v2 contracts or presenters, or Copilot tool handlers. Return domain values and let each surface presenter project its own wire result. + ## Adapt internal APIs -Use `defineInternalJsonRoute` for ordinary JSON routes. Explicitly declare the contract, session authentication policy, semantic operation, rate policy, error policy, input mapping, use case, and presenter when the wire result differs. +Use `defineInternalJsonRoute` for ordinary JSON routes. Explicitly declare the contract, authentication policy, semantic operation, rate policy, error policy, input mapping, use case, and presenter when the wire result differs. + +Use `internalSessionAuth` for session-only routes. Use `createInternalSessionOrExecutorAuth` only when the endpoint genuinely supports signed executor delegation; the semantic operation must then allow `delegated` principals from the `executor` service. Never turn an actorless legacy JWT into a fake session, owner, or user principal. -The internal adapter owns session authentication and internal response envelopes. It must not implement workspace authorization. Preserve internal-only analytics through `onSuccess` after application success. +The internal adapter owns authentication and internal response envelopes. It must not implement workspace authorization. Preserve internal-only analytics through `onSuccess` after application success. Keep the route module declarative. If several internal routes repeat authentication, parsing, error rendering, or response construction, improve the shared internal route builder instead of adding a domain-specific route wrapper. ## Adapt public or versioned APIs -Use the appropriate public/versioned route builder, such as `defineV2JsonRoute`, with API-key authentication, explicit operation rate policy, rollout policy, external error projection, and an external presenter. +Use the appropriate public/versioned route builder, such as `defineV2JsonRoute`, with API-key authentication, explicit semantic operation and rate policy, external error projection, input mapping, application use case, and an external presenter. V2 rollout admission is centralized by the builder; do not invent a route-local rollout policy. Authentication and HTTP formatting may differ from internal APIs; authorization and business behavior must not. Rate-limit using the credential or principal subject, never a billed owner. Resolve billing attribution only for billing, quota, or legacy required-user fields. @@ -180,7 +189,9 @@ Keep v1 middleware and routes unchanged unless explicitly included. ## Adapt Copilot -Create one domain-level Copilot application adapter instead of constructing delegated principals in every tool: +Copilot is a surface adapter, not a separate application layer. If an HTTP or other surface already uses an application use case, Copilot must call that exact use case rather than reimplementing protected business behavior under `lib/copilot`. + +Create one domain-level Copilot application adapter with `createCopilotApplicationAdapter` instead of constructing delegated principals in every tool: ```ts executeCopilotWidgetUseCase(context, renameWidget, input, { resourceId }) @@ -193,15 +204,16 @@ That adapter must: - Construct the shared delegated `Principal` in one place. - Optionally bind the canonical resource scope after trusted resolution. - Verify that the use case exposes a registered code-defined operation. +- Use the domain's exact immutable operation registry so operation-object membership and identity are checked centrally. - Call the application use case directly. Never construct authoritative delegation from model-provided workspace IDs, user IDs, operation IDs, resource scope, or permission tags. Model arguments are requested targets only and must be checked against trusted execution context and canonical data. -Tool handlers own argument aliases, resumable legacy names, abort checks, and tool-specific presentation. They must not query managers directly for protected operations or manually authorize. +Tool handlers own argument aliases, resumable legacy names, abort checks, tool-call reporting, and tool-specific presentation. They must not query managers directly for protected operations, manually authorize, or implement protected business behavior. If a Copilot-only compound action expresses real domain behavior, define a surface-neutral domain operation and application use case for it. -A Copilot reference helper may translate a path to a resource only by calling an authorized application resolver under the intended semantic operation. Passing a code-defined operation object is acceptable; passing a model-provided operation string is not. Reauthorizing during later execution is safe but redundant. When resolution and execution form one business operation, need a consistent snapshot, or appear repeatedly together, prefer a top-level application use case such as `renameWidgetByReference`. +A Copilot reference helper may translate a path to a resource only by calling an authorized application resolver under the intended semantic operation. Passing a code-defined operation object is acceptable; passing a model-provided operation string is not. An immediate same-request resolver followed by the operation may reuse one trusted principal, though the application operation still performs its own canonical authorization. Fresh authentication and authorization are required across lifecycle boundaries such as resumed tool calls, executor callbacks, queued or background work, upload control legs and finalization, durable completion, and long-running provider operations. When resolution and execution form one business operation, need a consistent snapshot, or appear repeatedly together, prefer a top-level application use case such as `renameWidgetByReference`. -Special composition roots may resolve one principal and deliberately thread it through several application calls or lower-level admission stages. Keep this exceptional and explicit; ordinary tools should use the shared execution adapter. +Surface adapters must not compose protected mutations. An atomic compound action requires one top-level semantic domain operation and application use case that owns the transaction and authoritative result. An explicitly best-effort application command may coordinate multiple operations only when it defines hard input and expansion caps, cancellation checkpoints, partial-result semantics, audit behavior, and rate/quota policy. Keep composition exceptional and explicit; ordinary tools should use the shared execution adapter. Map expected typed errors to safe tool results. Unknown errors must become generic system/retryable messages while retaining full causes in server logs. Never return raw database or storage errors to the model. @@ -247,10 +259,11 @@ Stop and report a missing design rather than weakening identity, authorization, Add focused tests for every migrated surface and principal kind allowed by the operation: - Application: allowed and disallowed roles, principal-kind rejection before canonical loading, workspace assertion mismatch, delegated scope, not found, conflict, no-op, and infrastructure propagation. +- Operation registry: role/workspace-key/principal-kind/delegated-service consistency and fail-fast rejection of invalid definitions. - Repository: canonical active lookup, workspace-predicated writes, archived resources, authoritative affected rows, and database error propagation. - Internal API: authentication before parsing, exact contract, typed errors, and surface analytics only after success. - Public API: personal and workspace keys, rate and rollout behavior, concealment, exact external envelope, and rate headers. -- Copilot or tools: trusted context, rejected forged scope, aliases and resume paths, permission re-check, safe errors, and unchanged tool result shapes. +- Copilot or tools: trusted context, exact registered operation membership, rejected forged scope, aliases and resume paths, permission re-check, safe errors, and unchanged tool result shapes. - Side effects: audit derives from authoritative results; shared notifications follow audit; neither occurs for rejection or no-op. Run at minimum: diff --git a/.cursor/rules/global.mdc b/.cursor/rules/global.mdc index bb8b8ed6dff..5535b598224 100644 --- a/.cursor/rules/global.mdc +++ b/.cursor/rules/global.mdc @@ -8,7 +8,12 @@ alwaysApply: true Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID. ## API Route Handlers -All API route handlers must be wrapped with `withRouteHandler` from `@/lib/core/utils/with-route-handler`. Never export a bare `async function GET/POST/...` — always use `export const METHOD = withRouteHandler(...)`. +All API route handlers must run inside `withRouteHandler`. Ordinary internal and v2 JSON/binary handlers use the shared route builders, which already apply it; never double-wrap them. Use raw `withRouteHandler` only for documented protocol or lifecycle exceptions. Never export a bare `async function GET/POST/...`. + +## Application Operation Boundary +Every protected read, write, canonical lookup, or authorization-sensitive reference resolution must enter through an authorized application use case. Surfaces authenticate and build a `Principal`, rate-limit, parse, map input, call the use case, and present their own result. They must not query protected data, decide resource authorization, implement business transactions, or record semantic audit. + +Define one stable semantic operation with its role, workspace-key policy, principal kinds, and delegated services. Internal, v2, Copilot, and trusted-tool adapters call the same use case when domain behavior is the same. Copilot uses `createCopilotApplicationAdapter`; it is not a separate protected business layer. Protected compound mutations require one top-level semantic application operation. Never substitute billing attribution, an uploader, creator, or key owner for the acting principal. Use the `migrate-application-operation` skill for new or migrated protected operations. ## Comments Use TSDoc for documentation. No `====` separators. No non-TSDoc comments. diff --git a/.cursor/rules/sim-architecture.mdc b/.cursor/rules/sim-architecture.mdc index 90bac74294d..af712e4fa6a 100644 --- a/.cursor/rules/sim-architecture.mdc +++ b/.cursor/rules/sim-architecture.mdc @@ -37,6 +37,21 @@ packages/ # @sim/* — audit, auth, db, logger, realtime-protocol - `apps/* → packages/*` only. Packages never import from `apps/*`. - `apps/realtime` avoids Next.js, React, the block/tool registry, provider SDKs, and the executor; never add `@/lib/webhooks/providers/*`, `@/executor/*`, `@/blocks/*`, or `@/tools/*` imports to any package it consumes. CI enforces this via `scripts/check-monorepo-boundaries.ts` and `scripts/check-realtime-prune-graph.ts`. +## Protected Application Operations + +Every real operation on protected or persisted data crosses one authorized application boundary: + +1. The surface authenticates its credential or trusted context and constructs a `Principal`. +2. A fixed, code-defined semantic operation declares minimum role, workspace-key policy, allowed principal kinds, and delegated services. +3. The application use case loads canonical context, checks asserted scope, authorizes current access, executes the manager/repository, projects semantic audit, and runs shared domain effects. +4. The surface presents its own internal, v2, Copilot, or tool result. + +Routes and tools must not query protected data, authorize resources, implement business transactions, or record semantic audit. Application modules must not import `app/api/**`, `next/server`, route contracts/presenters, or Copilot handlers. Copilot must call the same domain use case through `createCopilotApplicationAdapter`; do not create a second Copilot business implementation. Atomic compound mutations need one top-level semantic application operation rather than sequential surface calls. + +Ordinary internal and v2 routes use the shared JSON/binary route builders. Those builders already apply `withRouteHandler`; do not double-wrap them. Use raw `withRouteHandler` only for explicit protocol, streaming, large-body, multipart, or lifecycle exceptions, while keeping protected business work inside application use cases. + +Use the `migrate-application-operation` skill before creating or migrating a protected endpoint, tool command, or resource method. + ## Feature Organization Features live under `app/workspace/[workspaceId]/`: diff --git a/AGENTS.md b/AGENTS.md index f36d633df61..ae4d76e0de0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ You are a professional software engineer. All code must follow best practices: a - **Linting / Audit**: `bun run check:api-validation` must pass on PRs. Do not introduce route-local boundary Zod schemas, direct route Zod imports, or ad-hoc client wire types — see "API Contracts" and "API Route Pattern" below - **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID — no manual `withMetadata({ requestId })` needed -- **API Route Handlers**: All API route handlers (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) must be wrapped with `withRouteHandler` from `@/lib/core/utils/with-route-handler`. This provides request ID tracking, automatic error logging for 4xx/5xx responses, and unhandled error catching. See "API Route Pattern" section below +- **API Route Handlers**: All API route handlers (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) must run inside `withRouteHandler`. Ordinary internal and v2 handlers use the shared JSON/binary route builders, which already apply it; never double-wrap a builder. Use raw `withRouteHandler` only for documented protocol or lifecycle exceptions. See "API Route Pattern" below - **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments - **Styling**: Never update global styles. Keep all styling local to components - **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id` @@ -29,6 +29,17 @@ You are a professional software engineer. All code must follow best practices: a 3. Type Safety First: TypeScript interfaces for all props, state, return types 4. Predictable State: Zustand for global state, useState for UI-only concerns +### Application Operation Boundary + +- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through an authorized application use case. +- Define one stable semantic operation with its minimum role, workspace-key policy, allowed principal kinds, and delegated services. Internal APIs, v2 APIs, Copilot, and trusted tools call the same use case when the domain behavior is the same. +- Surface adapters authenticate and construct a `Principal`, apply request-rate policy, parse contracts, map input, and present results. They never query protected data, decide resource authorization, implement business transactions, or record semantic audit. +- Application use cases load canonical context, compare asserted scope, authorize current access, execute managers/repositories, project semantic audit, and trigger shared domain effects. Managers accept canonical IDs and scope, never credentials or principals. +- Copilot is a surface adapter. Use `createCopilotApplicationAdapter` and the domain's registered operation object; do not create Copilot-only authorization or business implementations. +- Protected compound mutations belong in one top-level semantic application operation. Do not sequence independently committing mutations in a route or tool adapter. +- Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal. Fail fast when the identity model or operation policy cannot express the caller. +- Use the `migrate-application-operation` skill whenever creating or migrating a protected endpoint, tool command, or resource method. + ### Root Structure ``` @@ -166,56 +177,49 @@ const provider = config as unknown as LegacyProvider ## API Route Pattern -Every API route handler must be wrapped with `withRouteHandler`. This sets up `AsyncLocalStorage`-based request context so all loggers in the request lifecycle automatically include the request ID. +Every route method must run inside `withRouteHandler`. Ordinary internal and v2 JSON/binary routes use `defineInternalJsonRoute`, `defineV2JsonRoute`, or the matching binary/stream builder. These builders already apply `withRouteHandler`; never wrap them again. Use raw `withRouteHandler` only for explicit protocol or lifecycle exceptions such as streaming, multipart control, large-body admission, OAuth, or public execution. -Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`: +Routes never `import { z } from 'zod'` and never define route-local boundary schemas. Declarative builders consume contracts and own authentication, admission, parsing, use-case execution, response validation, and error projection. A raw special route consumes the same contracts and validates with canonical helpers from `@/lib/api/server`, after authentication and cheap admission: - `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure - `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError` - `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError` - `isZodError(error)` — type guard. Routes never use `instanceof z.ZodError` -### Fully contract-bound route (`parseRequest`) +### Ordinary authorized JSON route ```typescript -import { createLogger } from '@sim/logger' -import type { NextRequest } from 'next/server' -import { NextResponse } from 'next/server' -import { createFolderContract } from '@/lib/api/contracts/folders' -import { parseRequest } from '@/lib/api/server' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -const logger = createLogger('FoldersAPI') - -export const POST = withRouteHandler(async (request: NextRequest) => { - const parsed = await parseRequest(createFolderContract, request, {}) - if (!parsed.success) return parsed.response - const { body } = parsed.data - logger.info('Creating folder', { workspaceId: body.workspaceId }) - return NextResponse.json({ ok: true }) +export const PATCH = defineInternalJsonRoute({ + contract: renameWidgetContract, + auth: internalSessionAuth, + operation: widgetOperations.rename, + rateLimit: internalRateLimits.none({ reason: 'Preserve existing internal behavior' }), + errorPolicy: internalWidgetErrorPolicy, + mapInput: ({ params, body }) => ({ + widgetId: params.widgetId, + assertedWorkspaceId: params.workspaceId, + name: body.name, + }), + useCase: renameWidget, + present: ({ widget }) => ({ success: true, widget }), }) ``` -### Composing with other middleware - -```typescript -export const POST = withRouteHandler(withAdminAuth(async (request) => { - return NextResponse.json({ ok: true }) -})) -``` +The contract, operation, and use case must agree at definition time. Authentication and request-rate admission happen before parsing; canonical loading and authorization happen in the application use case. The presenter returns only the surface success body. Routes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app/api/v1/middleware.ts` for auth, rate-limit, and workspace access. Compose contract validation inside that middleware — never reimplement auth/rate-limit per-route. -Never export a bare `async function GET/POST/...` — always use `export const METHOD = withRouteHandler(...)`. +Never export a bare `async function GET/POST/...`. Export the result of a shared builder or, for a documented special route, `withRouteHandler(...)`. ### Adding a new boundary feature end-to-end When adding a new route + client surface, follow this order. Each step has one place it lives. 1. **Author the contract first** in `apps/sim/lib/api/contracts/.ts` (or a subdirectory for large domains: `knowledge/`, `selectors/`, `tools/`). Define one schema per request slice (`params`, `query`, `body`, `headers`) and one for the response, then wrap with `defineRouteContract`. Export named type aliases (`z.input` for inputs, `z.output` for outputs). -2. **Implement the route** in `apps/sim/app/api//route.ts`. Auth always runs **before** `parseRequest` — never validate untrusted input before authenticating the caller. The route returns exactly the shape declared in `contract.response.schema`. -3. **Add the React Query hook** in `apps/sim/hooks/queries/.ts`. Use `requestJson(contract, input)` for the call. Build a hierarchical query-key factory (`all` → `lists()` → `list(workspaceId)` → `details()` → `detail(id)`) so invalidations can target prefixes. -4. **Use the hook in the component**. The mutation's `data` and `error` are fully typed from the contract; surface `error.message` (already extracted from the response body's `error` or `message` field by `requestJson`). +2. **Define the semantic operation and application use case** under `apps/sim/lib//application/`. The use case owns canonical loading, asserted-scope checks, current authorization, business behavior, semantic audit, and shared domain effects. +3. **Implement the route adapter** in `apps/sim/app/api//route.ts` with the appropriate shared builder. Declare auth, operation, rate policy, error policy, input mapping, use case, and presenter. Auth always runs **before** parsing. Use raw `withRouteHandler` only for an explicit special route, and keep protected work in application use cases. +4. **Add the React Query hook** in `apps/sim/hooks/queries/.ts`. Use `requestJson(contract, input)` for the call. Build a hierarchical query-key factory (`all` → `lists()` → `list(workspaceId)` → `details()` → `detail(id)`) so invalidations can target prefixes. +5. **Use the hook in the component**. The mutation's `data` and `error` are fully typed from the contract; surface `error.message` (already extracted from the response body's `error` or `message` field by `requestJson`). ### Schema review checklist (read the contract diff like a DB migration) @@ -473,4 +477,3 @@ For the full authoring instructions — SubBlock property tables, `condition`/`d Table column types are registry entries in `apps/sim/lib/table/column-types/` — one file per type owning its label, icon, storage cast, coercion, validation, conversion compatibility, formatting, and editor. `Record` on `registry.ts` and `registry.server.ts` is a compile-time completeness gate: adding a type to the union errors until both entries exist. Never add a `case 'sometype':` outside `column-types/` — a missing arm fails silently (a wrong `jsonbCast` breaks every filter on the column). If a consumer needs per-type knowledge, add a registry field. Use `/add-column-type` for the full procedure. - diff --git a/CLAUDE.md b/CLAUDE.md index fc63380d153..9bd7d678400 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ You are a professional software engineer. All code must follow best practices: a - **Linting / Audit**: `bun run check:api-validation` must pass on PRs. Do not introduce route-local boundary Zod schemas, direct route Zod imports, or ad-hoc client wire types — see "API Contracts" and "API Route Pattern" below - **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`. Inside API routes wrapped with `withRouteHandler`, loggers automatically include the request ID — no manual `withMetadata({ requestId })` needed -- **API Route Handlers**: All API route handlers (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) must be wrapped with `withRouteHandler` from `@/lib/core/utils/with-route-handler`. This provides request ID tracking, automatic error logging for 4xx/5xx responses, and unhandled error catching. See "API Route Pattern" section below +- **API Route Handlers**: All API route handlers (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`) must run inside `withRouteHandler`. Ordinary internal and v2 handlers use the shared JSON/binary route builders, which already apply it; never double-wrap a builder. Use raw `withRouteHandler` only for documented protocol or lifecycle exceptions. See "API Route Pattern" below - **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments - **Styling**: Never update global styles. Keep all styling local to components - **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id` @@ -30,6 +30,17 @@ You are a professional software engineer. All code must follow best practices: a 3. Type Safety First: TypeScript interfaces for all props, state, return types 4. Predictable State: Zustand for global state, useState for UI-only concerns +### Application Operation Boundary + +- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through an authorized application use case. +- Define one stable semantic operation with its minimum role, workspace-key policy, allowed principal kinds, and delegated services. Internal APIs, v2 APIs, Copilot, and trusted tools call the same use case when the domain behavior is the same. +- Surface adapters authenticate and construct a `Principal`, apply request-rate policy, parse contracts, map input, and present results. They never query protected data, decide resource authorization, implement business transactions, or record semantic audit. +- Application use cases load canonical context, compare asserted scope, authorize current access, execute managers/repositories, project semantic audit, and trigger shared domain effects. Managers accept canonical IDs and scope, never credentials or principals. +- Copilot is a surface adapter. Use `createCopilotApplicationAdapter` and the domain's registered operation object; do not create Copilot-only authorization or business implementations. +- Protected compound mutations belong in one top-level semantic application operation. Do not sequence independently committing mutations in a route or tool adapter. +- Never substitute a billing owner, uploader, creator, or API-key owner for the acting principal. Fail fast when the identity model or operation policy cannot express the caller. +- Use the `migrate-application-operation` skill whenever creating or migrating a protected endpoint, tool command, or resource method. + ### Root Structure ``` @@ -169,56 +180,49 @@ const provider = config as unknown as LegacyProvider ## API Route Pattern -Every API route handler must be wrapped with `withRouteHandler`. This sets up `AsyncLocalStorage`-based request context so all loggers in the request lifecycle automatically include the request ID. +Every route method must run inside `withRouteHandler`. Ordinary internal and v2 JSON/binary routes use `defineInternalJsonRoute`, `defineV2JsonRoute`, or the matching binary/stream builder. These builders already apply `withRouteHandler`; never wrap them again. Use raw `withRouteHandler` only for explicit protocol or lifecycle exceptions such as streaming, multipart control, large-body admission, OAuth, or public execution. -Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`: +Routes never `import { z } from 'zod'` and never define route-local boundary schemas. Declarative builders consume contracts and own authentication, admission, parsing, use-case execution, response validation, and error projection. A raw special route consumes the same contracts and validates with canonical helpers from `@/lib/api/server`, after authentication and cheap admission: - `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure - `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError` - `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError` - `isZodError(error)` — type guard. Routes never use `instanceof z.ZodError` -### Fully contract-bound route (`parseRequest`) +### Ordinary authorized JSON route ```typescript -import { createLogger } from '@sim/logger' -import type { NextRequest } from 'next/server' -import { NextResponse } from 'next/server' -import { createFolderContract } from '@/lib/api/contracts/folders' -import { parseRequest } from '@/lib/api/server' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -const logger = createLogger('FoldersAPI') - -export const POST = withRouteHandler(async (request: NextRequest) => { - const parsed = await parseRequest(createFolderContract, request, {}) - if (!parsed.success) return parsed.response - const { body } = parsed.data - logger.info('Creating folder', { workspaceId: body.workspaceId }) - return NextResponse.json({ ok: true }) +export const PATCH = defineInternalJsonRoute({ + contract: renameWidgetContract, + auth: internalSessionAuth, + operation: widgetOperations.rename, + rateLimit: internalRateLimits.none({ reason: 'Preserve existing internal behavior' }), + errorPolicy: internalWidgetErrorPolicy, + mapInput: ({ params, body }) => ({ + widgetId: params.widgetId, + assertedWorkspaceId: params.workspaceId, + name: body.name, + }), + useCase: renameWidget, + present: ({ widget }) => ({ success: true, widget }), }) ``` -### Composing with other middleware - -```typescript -export const POST = withRouteHandler(withAdminAuth(async (request) => { - return NextResponse.json({ ok: true }) -})) -``` +The contract, operation, and use case must agree at definition time. Authentication and request-rate admission happen before parsing; canonical loading and authorization happen in the application use case. The presenter returns only the surface success body. Routes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app/api/v1/middleware.ts` for auth, rate-limit, and workspace access. Compose contract validation inside that middleware — never reimplement auth/rate-limit per-route. -Never export a bare `async function GET/POST/...` — always use `export const METHOD = withRouteHandler(...)`. +Never export a bare `async function GET/POST/...`. Export the result of a shared builder or, for a documented special route, `withRouteHandler(...)`. ### Adding a new boundary feature end-to-end When adding a new route + client surface, follow this order. Each step has one place it lives. 1. **Author the contract first** in `apps/sim/lib/api/contracts/.ts` (or a subdirectory for large domains: `knowledge/`, `selectors/`, `tools/`). Define one schema per request slice (`params`, `query`, `body`, `headers`) and one for the response, then wrap with `defineRouteContract`. Export named type aliases (`z.input` for inputs, `z.output` for outputs). -2. **Implement the route** in `apps/sim/app/api//route.ts`. Auth always runs **before** `parseRequest` — never validate untrusted input before authenticating the caller. The route returns exactly the shape declared in `contract.response.schema`. -3. **Add the React Query hook** in `apps/sim/hooks/queries/.ts`. Use `requestJson(contract, input)` for the call. Build a hierarchical query-key factory (`all` → `lists()` → `list(workspaceId)` → `details()` → `detail(id)`) so invalidations can target prefixes. -4. **Use the hook in the component**. The mutation's `data` and `error` are fully typed from the contract; surface `error.message` (already extracted from the response body's `error` or `message` field by `requestJson`). +2. **Define the semantic operation and application use case** under `apps/sim/lib//application/`. The use case owns canonical loading, asserted-scope checks, current authorization, business behavior, semantic audit, and shared domain effects. +3. **Implement the route adapter** in `apps/sim/app/api//route.ts` with the appropriate shared builder. Declare auth, operation, rate policy, error policy, input mapping, use case, and presenter. Auth always runs **before** parsing. Use raw `withRouteHandler` only for an explicit special route, and keep protected work in application use cases. +4. **Add the React Query hook** in `apps/sim/hooks/queries/.ts`. Use `requestJson(contract, input)` for the call. Build a hierarchical query-key factory (`all` → `lists()` → `list(workspaceId)` → `details()` → `detail(id)`) so invalidations can target prefixes. +5. **Use the hook in the component**. The mutation's `data` and `error` are fully typed from the contract; surface `error.message` (already extracted from the response body's `error` or `message` field by `requestJson`). ### Schema review checklist (read the contract diff like a DB migration) @@ -490,4 +494,3 @@ For the full authoring instructions — SubBlock property tables, `condition`/`d Table column types are registry entries in `apps/sim/lib/table/column-types/` — one file per type owning its label, icon, storage cast, coercion, validation, conversion compatibility, formatting, and editor. `Record` on `registry.ts` and `registry.server.ts` is a compile-time completeness gate: adding a type to the union errors until both entries exist. Never add a `case 'sometype':` outside `column-types/` — a missing arm fails silently (a wrong `jsonbCast` breaks every filter on the column). If a consumer needs per-type knowledge, add a registry field. Use `/add-column-type` for the full procedure. - diff --git a/apps/sim/AGENTS.md b/apps/sim/AGENTS.md index 6c52c2df02d..ded59456771 100644 --- a/apps/sim/AGENTS.md +++ b/apps/sim/AGENTS.md @@ -11,6 +11,16 @@ These rules apply to files under `apps/sim/` in addition to the repository root 3. **Type Safety First**: TypeScript interfaces for all props, state, return types 4. **Predictable State**: Zustand for global state, useState for UI-only concerns +### Application Operation Boundary + +- Every protected read, write, canonical resource lookup, or authorization-sensitive reference resolution enters through an authorized application use case. +- Define one stable semantic operation with its role, workspace-key policy, principal kinds, and delegated services. Internal, v2, Copilot, and trusted-tool adapters call the same use case when domain behavior is the same. +- Surface adapters authenticate and build a `Principal`, rate-limit, parse, map, and present. They do not query protected data, authorize resources, implement business transactions, or record semantic audit. +- Application use cases canonicalize scope, authorize current access, execute managers/repositories, project semantic audit, and trigger shared domain effects. Application code stays surface-neutral and never imports `app/api/**`, `next/server`, route contracts/presenters, or Copilot handlers. +- Copilot uses `createCopilotApplicationAdapter`; it does not own separate protected business logic. Compound protected mutations require a top-level semantic application operation. +- Never substitute billing attribution, an uploader, creator, or API-key owner for the acting principal. Fail fast when the identity or policy cannot be represented. +- Use the `migrate-application-operation` skill for new and migrated protected endpoints, tool commands, and resource methods. + ### Root-Level Structure ``` @@ -130,44 +140,49 @@ output: z.unknown(), ## API Route Pattern -Routes never `import { z } from 'zod'` and never define route-local boundary schemas. They consume the contract from `@/lib/api/contracts/**` and validate with canonical helpers from `@/lib/api/server`: +Every route method must run inside `withRouteHandler`. Ordinary internal and v2 JSON/binary routes use `defineInternalJsonRoute`, `defineV2JsonRoute`, or the matching binary/stream builder. These builders already apply `withRouteHandler`; never wrap them again. Use raw `withRouteHandler` only for explicit protocol or lifecycle exceptions such as streaming, multipart control, large-body admission, OAuth, or public execution. + +Routes never `import { z } from 'zod'` and never define route-local boundary schemas. Declarative builders consume contracts and own authentication, admission, parsing, use-case execution, response validation, and error projection. A raw special route consumes the same contracts and validates with canonical helpers from `@/lib/api/server`, after authentication and cheap admission: - `parseRequest(contract, request, context, options?)` — fully contract-bound routes; parses params, query, body, and headers in one call. Pass `{}` for `context` on routes without route params, or the route's `context` argument when route params exist. Returns a discriminated union; check `parsed.success` and return `parsed.response` on failure. - `validationErrorResponse(error)` and `getValidationErrorMessage(error, fallback)` — produce 400 responses from a `ZodError`. - `validationErrorResponseFromError(error)` — when handling unknown caught errors that may or may not be a `ZodError`. - `isZodError(error)` — type guard. Routes never use `instanceof z.ZodError`. -### Fully contract-bound route (`parseRequest`) +### Ordinary authorized JSON route ```typescript -import { createLogger } from '@sim/logger' -import type { NextRequest } from 'next/server' -import { NextResponse } from 'next/server' -import { createFolderContract } from '@/lib/api/contracts/folders' -import { parseRequest } from '@/lib/api/server' -import { withRouteHandler } from '@/lib/core/utils/with-route-handler' - -const logger = createLogger('FoldersAPI') - -export const POST = withRouteHandler(async (request: NextRequest) => { - const parsed = await parseRequest(createFolderContract, request, {}) - if (!parsed.success) return parsed.response - const { body } = parsed.data - logger.info('Creating folder', { workspaceId: body.workspaceId }) - return NextResponse.json({ ok: true }) +export const PATCH = defineInternalJsonRoute({ + contract: renameWidgetContract, + auth: internalSessionAuth, + operation: widgetOperations.rename, + rateLimit: internalRateLimits.none({ reason: 'Preserve existing internal behavior' }), + errorPolicy: internalWidgetErrorPolicy, + mapInput: ({ params, body }) => ({ + widgetId: params.widgetId, + assertedWorkspaceId: params.workspaceId, + name: body.name, + }), + useCase: renameWidget, + present: ({ widget }) => ({ success: true, widget }), }) ``` +The contract, operation, and use case must agree at definition time. Authentication and request-rate admission happen before parsing; canonical loading and authorization happen in the application use case. The presenter returns only the surface success body. + Routes under `apps/sim/app/api/v1/**` use the shared middleware in `apps/sim/app/api/v1/middleware.ts` for auth, rate-limit, and workspace access. Compose contract validation inside that middleware — never reimplement auth/rate-limit per-route. +Never export a bare `async function GET/POST/...`. Export the result of a shared builder or, for a documented special route, `withRouteHandler(...)`. + ### Adding a new boundary feature end-to-end When adding a new route + client surface, follow this order. Each step has one place it lives. 1. **Author the contract first** in `apps/sim/lib/api/contracts/.ts` (or a subdirectory for large domains: `knowledge/`, `selectors/`, `tools/`). Define one schema per request slice (`params`, `query`, `body`, `headers`) and one for the response, then wrap with `defineRouteContract`. Export named type aliases (`z.input` for inputs, `z.output` for outputs). -2. **Implement the route** in `apps/sim/app/api//route.ts`. Auth always runs **before** `parseRequest` — never validate untrusted input before authenticating the caller. The route returns exactly the shape declared in `contract.response.schema`. -3. **Add the React Query hook** in `apps/sim/hooks/queries/.ts`. Use `requestJson(contract, input)` for the call. Build a hierarchical query-key factory (`all` → `lists()` → `list(workspaceId)` → `details()` → `detail(id)`) so invalidations can target prefixes. -4. **Use the hook in the component**. The mutation's `data` and `error` are fully typed from the contract; surface `error.message` (already extracted from the response body's `error` or `message` field by `requestJson`). +2. **Define the semantic operation and application use case** under `apps/sim/lib//application/`. The use case owns canonical loading, asserted-scope checks, current authorization, business behavior, semantic audit, and shared domain effects. +3. **Implement the route adapter** in `apps/sim/app/api//route.ts` with the appropriate shared builder. Declare auth, operation, rate policy, error policy, input mapping, use case, and presenter. Auth always runs **before** parsing. Use raw `withRouteHandler` only for an explicit special route, and keep protected work in application use cases. +4. **Add the React Query hook** in `apps/sim/hooks/queries/.ts`. Use `requestJson(contract, input)` for the call. Build a hierarchical query-key factory (`all` → `lists()` → `list(workspaceId)` → `details()` → `detail(id)`) so invalidations can target prefixes. +5. **Use the hook in the component**. The mutation's `data` and `error` are fully typed from the contract; surface `error.message` (already extracted from the response body's `error` or `message` field by `requestJson`). ### Schema review checklist (read the contract diff like a DB migration) @@ -228,4 +243,3 @@ export function useEntityList(workspaceId?: string) { - **Create `utils.ts` when** 2+ files need the same helper - **Check existing sources** before duplicating (`lib/` has many utilities) - **Location**: `lib/` (app-wide) → `feature/utils/` (feature-scoped) → inline (single-use) -