Skip to content

improvement(files): centralize operations across APIs and Copilot - #6392

Merged
TheodoreSpeaks merged 9 commits into
improvement/v2-platformsfrom
improvement/v2-route-handler-consolidation
Aug 8, 2026
Merged

improvement(files): centralize operations across APIs and Copilot#6392
TheodoreSpeaks merged 9 commits into
improvement/v2-platformsfrom
improvement/v2-route-handler-consolidation

Conversation

@TheodoreSpeaks

@TheodoreSpeaksTheodoreSpeaks commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • introduce a reusable, principal-aware application-operation foundation for canonical loading, workspace authorization, business execution, semantic audit, and post-success domain effects
  • migrate internal and v2 workspace-file operations to declarative route adapters while preserving their independent authentication, contracts, error envelopes, presenters, rate policies, and analytics
  • route Copilot file tools through shared application use cases using delegated principals derived only from trusted execution context
  • harden upload control legs with principal binding and current authorization while preserving workspace-key uploads and billing-owner attribution where legacy storage fields require it
  • share workspace filename validation across internal and v2 contracts and add a resource-agnostic migration skill for applying the architecture to other domains

Architecture

surface admission
-> authenticate trusted credential/context
-> construct Principal
-> operation rate policy
-> parse surface contract
-> authorized application use case
-> load canonical resource context
-> conceal asserted-scope mismatches
-> authorize the semantic operation
-> execute manager/repository behavior
-> project semantic audit
-> run shared post-success effects
-> surface-specific presenter
  • ApplicationOperation and WorkspaceOperation define one stable semantic operation, required role, accepted principal kinds, and workspace-key policy.
  • defineAuthorizedWorkspaceUseCase owns the shared authorization/execution/audit lifecycle; file-specific bindings only provide resource delegation policy.
  • Managers and repositories receive canonical identifiers and workspace scope, never credentials or principals.
  • Internal routes authenticate sessions with defineInternalJsonRoute; v2 routes authenticate API keys with the v2 JSON/binary adapters.
  • Copilot adapters normalize trusted tool context into delegated principals and call the same application use cases. Model-provided workspace, identity, operation, and permission fields are never authoritative.
  • Semantic audit is shared application behavior. Surface analytics such as captureServerEvent remain surface-specific.

Migrated scope

  • internal workspace-file JSON and binary routes
  • v2 workspace-file metadata, content, create, update, delete, restore, move, folders, sharing, downloads, and uploads
  • file bulk operations with bounded inputs and authoritative affected-resource reporting
  • workspace-file upload create, parts, completion, and cancellation control paths
  • Copilot file tools, resumable aliases, VFS reference resolution, and generated-output writes
  • shared principal, authorization, typed error, route-definition, presenter, rate-policy, and contract primitives

Compatibility and non-goals

  • v1 routes and v1 middleware are unchanged
  • no database migration is introduced
  • internal and v2 wire contracts remain independently defined where their request or response shapes differ; shared primitives carry common validation invariants
  • workspace API keys may perform explicitly allowed read/write operations, including uploads, but cannot exceed the write ceiling
  • billing-owner identity is used only for billing or required legacy attribution fields, never for authorization, rate-limit identity, delegated identity, or human audit attribution
  • existing in-flight upload sessions retain an explicit compatibility path; newly created sessions use server-authored principal binding
  • large-body, binary, streaming, bulk, and upload operations retain lifecycle-specific admission rather than being forced through one JSON pipeline

Migration tooling

  • replace the file-only guide with migrate-application-operation
  • cover internal APIs, public/versioned APIs, Copilot, and other trusted tool runtimes
  • require every protected read, mutation, canonical lookup, and authorization-sensitive reference resolution to enter through an application use case
  • permit surface helpers only for trusted principal normalization, argument translation, code-defined operation selection, application invocation, and presentation

Validation

  • focused Vitest coverage across principals, authorization, application use cases, internal/v2 route adapters, file operations, upload lifecycle behavior, contracts, and Copilot tools
  • bunx turbo run type-check --filter=sim --filter=@sim/auth
  • bun run lint
  • bun run check:api-validation:strict
  • bun run check:openapi
  • bun run skills:check
  • git diff --check

Type of Change

  • Improvement

Checklist

  • Code follows project style guidelines
  • Self-reviewed changes
  • Tests added or updated and passing
  • No new warnings introduced
  • v1 behavior remains out of scope
  • No database migration introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

@TheodoreSpeaks
TheodoreSpeaks requested a review from a team as a code ownerAugust 7, 2026 23:48
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
docsReadyReadyPreviewAug 8, 2026 9:11am

Request Review

@cursor

cursorBot commented Aug 7, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Changes touch authorization, upload finalization, and multiple API/tool entry points for protected file data; regressions could affect access control or idempotent upload completion across surfaces.

Overview
This PR moves workspace file behavior (reads, writes, uploads, renames, deletes, downloads, and internal tool paths) behind shared application use cases with explicit Principal identity, instead of route-local permission checks and direct manager calls.

Internal upload routes now call upload-session application helpers that bind sessions to the authenticated principal, re-authorize workspace-file control legs, and finalize through finalizeUploadPurpose with principal-aware audit and workspace-key billing attribution only for legacy columns.

V2 file routes (metadata, content, download/rename/delete) are wired through defineV2JsonRoute / defineV2BinaryRoute, semantic fileOperations, operation rate limits, and concealed authorization (forbidden → not found).

Executor and internal file-manage tools construct delegated principals and call the same use cases (create, read, update, resolve references); query userId is no longer used as the auth identity.

Also adds a migrate-application-operation agent skill/command and extends the ship audit list with check:openapi and check:sql-date-binding. V1 routes and schema are unchanged per the PR scope.

Reviewed by Cursor Bugbot for commit 0ab6d68. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment threadapps/sim/app/api/public-api-route-handler.ts
@TheodoreSpeaks
TheodoreSpeaksforce-pushed the improvement/v2-route-handler-consolidation branch from d55345d to df5d4d6CompareAugust 8, 2026 00:10
@TheodoreSpeaks
TheodoreSpeaks changed the base branch from improvement/v2-endpoints to improvement/v2-platformsAugust 8, 2026 00:10
@TheodoreSpeaks
TheodoreSpeaksforce-pushed the improvement/v2-route-handler-consolidation branch 2 times, most recently from e68c248 to 6448be1CompareAugust 8, 2026 00:18
Comment threadapps/sim/lib/workspace-files/application/rename-workspace-file.ts Outdated
@TheodoreSpeaks
TheodoreSpeaksforce-pushed the improvement/v2-route-handler-consolidation branch from 6448be1 to b528d5eCompareAugust 8, 2026 00:35
Comment threadapps/sim/lib/copilot/tools/handlers/vfs-mutate.ts
Comment threadapps/sim/app/api/function/execute/route.ts Outdated
Comment threadapps/sim/lib/uploads/upload-session/application.ts
Comment threadapps/sim/app/api/files/uploads/purposes.ts
@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile-apps

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b061854. Configure here.

@greptile-apps

greptile-appsBot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR centralizes workspace-file operations behind principal-aware application services while preserving surface-specific authentication, contracts, and presentation.

  • Adds shared operation definitions, authorization wrappers, typed principals, rate policies, and transactional file use cases.
  • Migrates internal, v2, upload, and Copilot adapters to the shared application boundary.
  • Adds focused authorization, route, upload-lifecycle, and file-operation tests.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

FilenameOverview
apps/sim/lib/core/application/authorized-workspace-use-case.tsIntroduces the common lifecycle for principal validation, canonical context loading, authorization, execution, audit projection, and post-success effects.
apps/sim/lib/core/application/workspace-authorization.tsCentralizes session, personal-key, workspace-key, and delegated-principal authorization with canonical workspace and delegation-scope checks.
apps/sim/lib/workspace-files/application/rename-workspace-file.tsRoutes file rename behavior through the shared authorized application boundary and authoritative manager operation.
apps/sim/lib/uploads/upload-session/application.tsAdds application-level authorization and immutable identity binding around multipart upload control operations.
apps/sim/lib/copilot/auth/file-delegation.tsConstructs short-lived delegated file principals from trusted Copilot execution context.
apps/sim/lib/copilot/application/execute-file-use-case.tsAdapts Copilot file tools to shared application use cases and authorized reference resolution.
apps/sim/lib/api/server/routes/v2-json-route.tsAdds a declarative v2 JSON route adapter that keeps API-key authentication, rate policy, parsing, and response projection outside application logic.
packages/auth/src/principal.tsDefines explicit principal variants and structured audit attribution shared by application operations.

Sequence Diagram

sequenceDiagram
participant Surface as Internal / v2 / Copilot adapter
participant Auth as Authentication adapter
participant App as Authorized file use case
participant Context as Canonical context loader
participant Authz as Workspace authorization
participant Manager as File manager / repository
Surface->>Auth: Validate surface credential or trusted context
Auth-->>Surface: Principal
Surface->>App: Principal + operation input
App->>Context: Load canonical file/workspace context
Context-->>App: Canonical identifiers and policy state
App->>Authz: Authorize principal for semantic operation
Authz-->>App: Allowed
App->>Manager: Execute scoped transaction
Manager-->>App: Authoritative result
App-->>Surface: Typed result
Surface-->>Surface: Render surface-specific response
Loading

Reviews (5): Last reviewed commit: "docs(skills): generalize application ope..." | Re-trigger Greptile

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile-apps

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 58d8d19. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 72ae9f5. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 3700dff. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@TheodoreSpeaksTheodoreSpeaks changed the title improvement(files): unify rename authorizationimprovement(files): centralize operations across APIs and CopilotAug 8, 2026
@TheodoreSpeaks
TheodoreSpeaks merged commit b177b3a into improvement/v2-platformsAug 8, 2026
5 checks passed
@TheodoreSpeaks
TheodoreSpeaks deleted the improvement/v2-route-handler-consolidation branch August 8, 2026 09:16
TheodoreSpeaks added a commit that referenced this pull request Aug 11, 2026
…updated docs based on openapi spec (#5273)
* v0.6.29: login improvements, posthog telemetry (#4026)
* feat(posthog): Add tracking on mothership abort (#4023)
Co-authored-by: Theodore Li <theo@sim.ai>
* fix(login): fix captcha headers for manual login (#4025)
* fix(signup): fix turnstile key loading
* fix(login): fix captcha header passing
* Catch user already exists, remove login form captcha
* improvement(external-endpoints): v2 versions with clean signatures + updated docs
* feat(usage): accept X-API-Key on usage-logs list + export
/api/users/me/usage-logs and /export now use checkHybridAuth — the same
auth /api/users/me/usage-limits already accepts — so external monitors
can read summary.bySourceCredits (the source breakdown of usage-limits'
aggregate currentPeriodCost) instead of estimating Copilot spend by
subtraction. Workspace-scoped keys are pinned to their own workspace's
slice of the ledger: the filter defaults to the key's workspace and an
explicit mismatch 403s. Both endpoints documented in openapi-core.json.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* feat(billing): dedicated v2 usage endpoints; keep internal usage routes session-only
Replaces the earlier X-API-Key enablement on /api/users/me/usage-logs
with a dedicated public surface, so the internal Billing-settings
endpoints can evolve with the UI while external monitors get a stable
versioned contract:
- GET /api/v2/billing/usage — current-billing-period summary with
bySourceCredits (the source breakdown external monitors need to watch
e.g. Copilot consumption without estimating by subtraction), plus
limitCredits and plan
- GET /api/v2/billing/usage/logs — cursor-paged credit ledger in the v2
envelope
- workspace-scoped keys are pinned to their own workspace's slice;
personal keys read the account ledger
The public wire is credits-only: usage-logs rows now carry a hasCost
boolean instead of dollarCost (the Billing UI only needed the >0
signal), and the rateLimit block is removed from the usage-limits
response and docs (deploy-modal tab relabeled accordingly).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* feat(docs): validate OpenAPI specs against the Zod contracts in CI
The specs in apps/docs are hand-authored because they carry what Zod
never defines — error envelopes, status codes, prose, examples — so
they can't be generated; check:openapi validates them instead:
- spec integrity: $refs resolve, operationIds unique, 2xx documented,
no orphaned component schemas
- v2 conventions: every /api/v2 operation documents 401 + 429 and every
4xx/5xx resolves to the canonical { error: { code, message } } envelope
- contract cross-check: contracts are auto-discovered from
lib/api/contracts/v2 (each carries its method + path); doc<->contract
coverage both ways, query/body/response field diffs via z.toJSONSchema
- examples: documented request/response examples must parse with the
matching contract's actual Zod schemas
First run caught real drift, fixed here: 16 stale orphaned schemas in
the core spec, the v2 billing ops referencing v1-shaped error
components, deploy/rollback examples missing the required nullable
lifecycle keys, CreateTableBody missing folderId, a legacy-grammar
delete-rows example, and four knowledge document ops missing their
required workspaceId query param.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* fix(docs): recursive field diff in check:openapi + the deep drift it found
A mutation test showed the doc<->contract field diff only compared
top-level properties, so a typo inside the { data } envelope passed.
The diff now descends through matching object properties and array
items (both sides must expose a property set — passthrough contracts
and prose-only docs end the descent instead of false-positive), with
the Zod JSON-schema root doubling as the $defs context.
Deep drift it immediately caught, fixed here: select-column config
(options/multiple) missing from every tables column schema, AddColumnBody
hand-rolling a third column shape (now composed from ColumnInput, with
position/workflowGroupId as the per-op extensions the contracts actually
admit), chunking strategyOptions undocumented, and the deployment
lifecycle fields (activeDeployment/latestDeploymentAttempt) missing from
DeploymentState.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* fix(security): close the triggerType rate-limit bypass on workflow execute
Caller-supplied triggerType flowed unchecked into preprocessExecution,
whose checkRateLimit default turns OFF for 'manual'/'chat' — so any
API-key caller, and any anonymous public-API caller billed to the
workspace owner, could execute unthrottled by sending
{"triggerType":"manual"} (async runs also skipped the worker-side check
via admissionCompleted). External callers may now only send the
redundant 'api' value; internal JWT callers ('workflow'/'mcp') are
unaffected.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* refactor(execution): extract enqueue/status/cancel into shared libs
Prepares the v2 execution surface: handleAsyncExecution's queue logic
moves to lib/workflows/executor/enqueue-execution.ts (slot/claim
semantics encoded in a discriminated outcome, not HTTP statuses), the
execution-status read to execution-status.ts, and the order-sensitive
cancel machinery to lib/execution/cancel-workflow-execution.ts. The v1
routes re-render identically — their suites pass unmodified.
Also: preprocessExecution gains rateLimitCounter ('sync'|'async') and
its 429 now carries code RATE_LIMIT_EXCEEDED + retryAfterMs (previously
indistinguishable from the concurrency 429 and Retry-After was
discarded); and the duplicate cancel contract in contracts/logs.ts is
unified on the full 5-value reason enum — its narrower copy made
requestJson throw a client ZodError when cancelling a paused HITL run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* feat(execution): callable execution service + structured error classifier
executeWorkflowService composes the same libs the v1 route holds inline
(call-chain guard, execution-id claim, LoggingSession, preprocessing,
deployed-state load + file-field processing, timeout-bound
executeWorkflowCore, output hydration/compaction) for the deployed-state
caller class — the seam the v2 execute route and in-process internal
callers share, making the HTTP endpoint syntactic sugar.
classifyExecutionError stops discarding the block context that
buildBlockExecutionError already attaches at throw sites: failed runs
now yield {message, code, blockId, blockName, blockType} with a stable
append-only code enum (TIMEOUT/CANCELLED/USAGE_LIMIT_EXCEEDED/
INVALID_INPUT/BLOCK_EXECUTION_FAILED/CHILD_WORKFLOW_FAILED/
OUTPUT_TOO_LARGE/EXECUTION_FAILED), so callers route on error class
instead of substring-matching messages — the single place raw errors
are interpreted.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* feat(api): POST /api/v2/workflows/[id]/execute
Thin route over executeWorkflowService: X-API-Key or anonymous
public-API auth (sync/stream only for anonymous), strict body with
body-flag async (no mode headers on v2), SSE passthrough for stream,
and the execution resource response — executionId always present,
in-band run failures are status:'failed' with the structured
{message, code, blockId, blockName, blockType} error, sync timeout is
status:'failed' + TIMEOUT instead of v1's 408, and a Response block's
payload stays inside output (authors never control response
status/headers on this origin). Async debits the async bucket and the
202 statusUrl points at the v2 executions resource. Adds
CLIENT_CLOSED_REQUEST/SERVICE_UNAVAILABLE to the v2 error codes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* feat(api): v2 executions status + cancel with queued backfill
GET /api/v2/workflows/[id]/executions/[executionId] is the single
status URL for sync and async runs: before the async worker writes the
durable log row, status is backfilled from the job queue (deterministic
job id) as 'queued'/'running' — closing v1's 202-to-pickup 404 window —
and failed runs carry the structured error object. POST .../cancel
renders the shared cancellation lib in the v2 envelope with the
tightened 5-value reason enum. Both authenticate via the shared
resolveV2WorkflowAccess (X-API-Key, authz masked as 404,
allowPersonalApiKeys honored).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* feat(execution): workflow tool + MCP bridge run in-process
workflow_executor (workflow-as-agent-tool) short-circuits in executeTool
through WorkflowBlockHandler — the same invocation boundary canvas child
workflows use — mirroring the deployed_block_executor precedent. The
MCP serve bridge calls executeWorkflowService directly instead of
fetching its own execute endpoint; deployment-version pinning, MCP
response-size rejection, and the actor override become typed options
instead of header sniffing. Both callers drop the double admission slot
and duplicate top-level log row the HTTP hop cost, and failed child
runs now surface the structured error + child executionId so parents
and MCP clients can route on error class and hand providers a
reproducible handle.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* feat(infra): CORS + CSP coverage for the v2 execute path
/api/v2/workflows/:id/execute gets the same wildcard-origin,
credential-free CORS policy as v1 (the default credentialed policy
would block browser API-key calls and open a cookie CSRF surface) with
X-Sim-Stream-Protocol allowed and no X-Execution-Mode (async is
body-selected on v2), plus the COEP/COOP/CSP header block.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* feat(ui): deploy modal + copilot advertise the v2 execute surface
All 20 API-tab snippets move to POST /api/v2/workflows/{id}/execute with
the nested {"input": ...} body, async as the "async": true body flag
(X-Execution-Mode gone), status polling against the v2 executions
resource, the third tab renamed Usage and pointed at
/api/v2/billing/usage, and {data} envelope unwraps in the printed
responses. Fixes the latent baseUrl derivation
(endpoint.split('/api/workflows/')) that would have silently built
garbage URLs under a v2 endpoint, and deletes dead code (exampleCommand
across 3 sites, getAsyncExampleTitle). Copilot deploy/manage/serializer
endpoint builders and the api_trigger bestPractices example follow (the
latter also drops its hardcoded staging host).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* docs(api): document the v2 execution surface
Adds execute, execution status, and cancel to openapi-v2-workflows.json
with the structured ExecutionError schema (append-only code enum + block
attribution) and the ExecutionResource contract, documenting the rules
that differ from v1: modes are body-selected, a failed run is HTTP 200
with status 'failed', an executionId always means data (never the error
envelope), queued status is visible immediately, and Response-block
payloads stay inside output. Registers the three pages in the generated
workflows meta.json and bumps the route-count baseline.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* feat(api): gate the whole /api/v2 surface behind one flag; UI stays on v1
Every v2 route now runs exactly one check immediately after auth —
v2ApiGateError — and answers 404 when the `v2-api` flag is off, so the
surface is invisible until it is deliberately rolled out. The gate is
keyed on userId only: a workspace/org-keyed check would have to read
membership for a caller-supplied id before authorization runs, and its
404-vs-403 split would leak cohort membership (the trap the per-domain
table gate worked around by running late). The two executions routes
inherit it from the shared access resolver; the tables-specific gate is
removed so no route checks twice.
`tables-v2-api` stays, now gating only the internal predicate-grammar
route /api/table/[tableId]/query — note v2 tables routes move to the
unified flag, so enabling them is a `v2-api` decision now.
Reverts the deploy modal, copilot handlers, and api_trigger example to
the v1 execute endpoint: v1 works unchanged, and the UI must not
advertise a surface most users would get a 404 from.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CiHhAk2R1NryaS3R8n2yFz
* fix(executor): restore child-cost aggregation dropped by the staging merge
Staging's custom-block rewrite deleted `aggregateChildCost` from
workflow-handler.ts, and git merged that file cleanly — but this branch's
workflow-tool-runner.ts, added for the v2 execute migration, still imports it.
A silent semantic conflict: no marker, broken build.
Taking staging's rewrite is correct, so the helper is defined locally in its
one remaining consumer rather than resurrected in the file staging just
rewrote. Same four lines over the still-exported `calculateCostSummary`, so a
failed child workflow keeps billing the hosted-key spend it consumed instead
of reporting $0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(tables): make lib/table/orchestration the single implementation (#6134)
* refactor(orchestration): move the shared error contract out of lib/workflows
OrchestrationErrorCode and statusForOrchestrationError are the contract every
lib/[resource]/orchestration module returns against, but they lived inside the
workflows module, so resource-neutral code (lib/folders) already had to import
from a workflow path. Moved to lib/core/orchestration/types.
Adds a 'locked' class mapping to 423. Both tables and workflows have a lock
that forbids a mutation, and each caller was translating that to a status
itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(tables): make lib/table/orchestration the single implementation
Column update was implemented four times — the UI route, v1, v2, and the
copilot table tool — each calling the same column services but owning its own
guards, error mapping, and audit. The copies had drifted, and the drift was the
bug: v2 was missing both guards, only the copilot copy minted stable option
ids, and only v1/v2 audited.
performUpdateTableColumn, performDeleteTable, and performDeleteTableRow now own
that logic; all ten call sites reduce to auth, parse, call, render. The guards
are asserted once in lib/table/orchestration rather than four times against
four routes.
Behavior this consolidates, previously true on only some paths:
- The typeChanging guard. updateColumnType early-returns on an unchanged type
and drops any options sent with it, so restating the current type alongside
new options silently discarded them. v2 had no guard at all and, since its
contract shares v1's body schema, accepted options and ignored them.
- The select-unique guard. Each write is its own locked transaction, so a
rename or type change paired with a constraint write that is going to fail
commits first and then throws, half-applying the schema change.
- Stable select-option ids. Cells reference the option id, so an edit that
re-sends an option by name has to reuse it or every cell holding it is
orphaned. Only the copilot path did this; normalizeSelectOptionsInput moves to
lib/table/select-options and now covers every caller. It preserves a supplied
id, so it is a no-op for the fully-formed options the HTTP contracts accept.
- required forwarded into the type and options writes, so a conversion
validates against the constraint the same request is setting.
- An audit on every successful update. The UI route and the copilot tool
emitted none.
- Single-row delete through the row service. v2 did a raw db.delete, skipping
assertRowDelete and deleteOrderedRow, so a delete-locked table returned 200
and the row-count bookkeeping never ran.
- The delete actor handed to deleteTable, which audits only when a row was
actually archived. v1 and v2 omitted it and audited themselves outside that
check, emitting TABLE_DELETED for a no-op delete of an archived table.
Failure classes come back as OrchestrationErrorCode; v2 renders them through a
new v2ErrorForOrchestration, mirroring statusForOrchestrationError on the v1
and UI surfaces, so a given failure maps to the same status everywhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(tables): bind the column-update tests to the orchestration function
The base's route tests assert which column service each payload reaches — the
behavior that now lives in performUpdateTableColumn. They mocked the `@/lib/table`
barrel; the orchestration module imports the service directly, so they mock that
too and keep asserting the same thing through the extracted implementation.
The orchestration tests move onto the base's semantics: writes address the
stable column id, a rename rides inside the write it accompanies rather than
running first, and the currency guards replace the non-select options guard the
service now owns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(copilot): drop the column-type import the delegation made dead
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(tables): move the audit log out of the table service
`lib/table/service.ts` wrote its own audit rows, so whether an operation was
audited depended on which function a caller reached for rather than on a user
having performed it. That is what let v1 and v2 audit a no-op delete, and what
made `deleteTable`'s optional `actingUserId` double as an audit opt-out flag.
Worse, most sites fell back to `actingUserId ?? createdBy`, so an unattributed
call was logged against the table's *creator*. The copilot `mv` path passed no
actor at all: renaming someone else's table recorded them as the renamer.
Audit now lives in the orchestration functions — performDeleteTable,
performRenameTable, performMoveTableToFolder, performUpdateTableLocks — and
the services just write. Internal callers (folder cascade, import rollback)
keep calling the service and are silent by construction rather than by
remembering to omit an argument.
Two services now return what the audit needs: `deleteTable` reports whether it
actually archived a row, so a repeat delete logs nothing; `updateTableLocks`
returns the before/after locks, since only the locked write can observe the
transition its description names.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(tables): restore audit provenance and conflict status in orchestration
Moving the audits into the orchestration functions dropped three things the
routes had been carrying, and added one the orchestration now owns twice.
- The v1 and v2 column-update routes passed `request` to `recordAudit`, so
their audit rows recorded the caller's IP and user-agent. The orchestration
function had no way to receive it. Every table orchestration function now
takes an optional `OrchestrationRequestContext` and every HTTP route
forwards it; the copilot and VFS callers, which have no request, omit it.
- `classifyTableMutation` matched `TableConflictError` on "already exists"
appearing in the message and reported it as `validation`, turning the UI
route's 409 on a duplicate table rename into a 400. It now matches the type,
the way `performRestoreTable` already did.
- `captureServerEvent` ran on every delete while the audit was gated on a row
actually being archived, so a repeat delete of an archived table still
reported `table_deleted`. Both now hang off the same evidence.
- The copilot delete path kept its own `captureServerEvent` from when the
service did not emit one, double-counting every copilot table delete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a
* fix(tables): say which type a no-op column update restated
A copilot `update_column` payload whose only content was the column's current
type used to return success with the live schema, while the v1, v2, and UI
routes rejected the same payload with "No updates specified". Delegating to
`performUpdateTableColumn` unified them onto the routes' rejection — correct,
but the message tells the caller its request was empty when it named a type.
The orchestration function now reports the same thing `updateColumnType` reports
when it loses this race concurrently: the column is already that type, re-issue
without the type change. An empty payload still reads "No updates specified".
Drops the copilot's `outcome.table ?? tableForUpdate` fallback with it — the
comment described the no-op that can no longer reach that line, and a success
always carries a table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a
* refactor(tables): classify failures by type instead of by message text
The table module decided HTTP statuses by searching error messages for
phrases. `VALIDATION_MESSAGE_FRAGMENTS` and `ROW_WRITE_ERROR_PATTERNS` held 32
substrings between them, and fifteen more lists were inlined in routes — 83
matchers over 17 files, each its own copy of the guesswork and already drifted
apart. It made message wording load-bearing: `TableRowLimitError`'s own doc
comment noted that its text had to contain "row limit" for a route to answer
400, and adding "already exists" to a rename message silently demoted a 409 to
a 400 (the bug fixed one commit ago, by adding another special case).
Services now throw `OrchestrationError`, which carries the transport-neutral
`OrchestrationErrorCode` the layers above already speak. Classification is one
`instanceof` in `orchestrationErrorResponse` (UI + v1) and
`v2CaughtOrchestrationError` (v2). Every pattern list is gone. Wording is free
to change; an unclassified error still becomes a generic 500, which is what an
unexpected fault should be.
`asOrchestrationError` walks the `cause` chain rather than testing the caught
value directly: drizzle wraps a throw raised inside a transaction callback in a
`DrizzleQueryError` whose own message is the failed SQL, so a bare `instanceof`
would drop every failure raised inside `withLockedTable`. That is the same
reason `rootErrorMessage` had to dig for a root cause before.
Three throws stay bare `Error` deliberately — `Table ID mismatch`, `Workspace
ID mismatch`, and `Failed to build upsert conflict predicate` are internal
invariants no consumer classified, and they keep falling through to a 500.
`Insufficient capacity` was in the pattern list with no producer anywhere in
the codebase.
Status changes, all deliberate:
- `'forbidden'` joins the code union so the table-row-limit ceiling keeps its
403; without it this refactor would have flattened it to 400.
- import-async's table-limit rejection: 400 -> 403, matching the two other
create routes it had drifted from.
- Renaming a table to an invalid name: 500 -> 400. `validateTableName`
messages don't contain "Invalid", so no matcher ever caught them.
- Restoring a table that isn't archived, or into an archived workspace:
500 -> 400.
- A duplicate *column* name stays `validation`/400 rather than becoming a 409
like a duplicate table name. Both v1 and the orchestration have always
answered 400 for it; changing a published status is not this refactor's job.
The twelve tests that changed were asserting the substring mechanism itself,
constructing plain `Error`s with magic strings. They now assert the real
contract, plus new cases pinning that identical wording carrying no
classification stays internal and keeps its message off the wire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* feat(api): add v2 endpoints for MCP servers, skills, custom tools, folders, and credentials (#6150)
* feat(api): add v2 endpoints for MCP servers, skills, custom tools, folders, and credentials
* fix(api): correct credential role, skill permission bar, MCP url identity, and custom-tool conflict mapping
* fix(api): align credential mutation gating, provider-outage status, and unique-violation conflicts
* fix(api): close unique-violation, revival, orphan-write, and env-rename gaps
* fix(api): treat every provider-outage code as unavailable on create and update
* fix(credentials): use the shared outage predicate on the session update path
* fix(contracts): anchor the predicate double-cast annotation to the cast
`check:api-validation:strict` counted 9 unannotated double-casts against a
baseline of 8, failing CI. The predicate leaf schema was annotated, but the
annotation sat above the declaration while the checker anchors on the line
carrying the cast — five lines below, at the close of the object literal. The
scanner walks back at most three lines and stops at the first non-comment one,
so it hit `value: z.unknown().optional(),` and never saw the reason.
Splitting the object schema from the cast puts them adjacent, so the existing
reason binds. No behavior change — the cast, the schema, and the reasoning are
unchanged.
Also lowers the rawJsonReads ratchet 6 -> 5 to match the current count, which
had drifted down; leaving it high lets a removed raw read silently come back.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(skills): point the orchestration error contract at its moved module
#6150 branched before #6134, so skill-lifecycle.ts imports
@/lib/workflows/orchestration/types — the module #6134 moved to
@/lib/core/orchestration/types. Git merged a file deletion on one side with a
new file referencing it on the other: no textual conflict, broken build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(knowledge): make lib/knowledge/orchestration the single implementation (#6154)
* refactor(knowledge): make lib/knowledge/orchestration the single implementation
Knowledge base create was implemented four times — the internal route, v1, v2,
and the copilot tool — and the orchestration around the shared write had
drifted. Extract it the same way lib/table/orchestration was: services write,
orchestration decides which writes run, guards them, audits them, and returns a
transport-neutral failure.
Behavior converged, not preserved:
- One chunking default (DEFAULT_CHUNKING_CONFIG). The agent defaulted minSize to
1 against the API's 100, so identical input produced differently-chunked
knowledge bases depending on who created it. The agent path now chunks at 100.
- Every successful mutation is audited inside the orchestration function. The
copilot tool called recordAudit zero times, so agent-created knowledge bases,
document uploads, updates and deletes left no audit trail at all.
- Failures classify by class, not by message text. The knowledge service errors
are OrchestrationError subclasses and storage-quota rejections throw a shared
StorageLimitExceededError, replacing four separate message greps for
"already exists" / "does not have permission" / "storage limit".
delete_connector reported the opposite of what happened. It reached the route
through an internal HTTP self-call that sent no query string, so the route's
keep-documents default always applied while the agent told the user the
documents had been removed. The self-call is gone — all four connector
operations run in-process — and the orchestration returns the real counts.
Also:
- OrchestrationErrorCode gains 'payload_too_large' (413 / PAYLOAD_TOO_LARGE).
Without it, dropping the storage-limit message match would have regressed the
documented 413 on knowledge base create and document upload to a 500.
- messageForOrchestrationError renders a route's own wording for an unclassified
fault, so a driver's message no longer reaches the client on a 500.
- v1 and v2 knowledge base update now forward actorUserId, which the service
requires for a workspace move; both omitted it.
- The connector DELETE route reads deleteDocuments through parseRequest. Its
contract declared z.boolean(), which would have rejected the string a query
param actually is.
- Drop the 409 from POST /api/v2/knowledge/{id}/documents in the OpenAPI spec.
Nothing on the upload path throws a conflict; it was only ever reachable by
the message match this change removes.
Behavior change worth noting: a v1/v2 PUT carrying only the workspaceId scope
field and no actual updates now returns 400 rather than 200 with the unchanged
knowledge base.
Deliberately deferred: document update remains internal-only. Extracting
performUpdateKnowledgeDocument makes exposing it on v1/v2 a contract and a route
away, but that is a new public surface rather than part of this consolidation.
* fix(knowledge): make connector create atomic and stop flattening failures
Review round 1 on #6154.
- Resolve the billing payer before the connector is committed, not after. A
malformed attribution header rejected post-commit left a live connector behind
a 500, and a retry created a duplicate plus duplicate sync work. Manual sync
resolves before writing its audit for the same reason.
- Let the source-config validator carry its own failure class. Collapsing every
rejection to `validation` flattened the connector PATCH route's 401 (stale
stored credential) and 409 (missing workspace context) into a 400.
- Add `unauthorized` to OrchestrationErrorCode. It is the class that 401 was
already expressing on this route, and the v2 vocabulary already had
UNAUTHORIZED; only the shared union was missing it.
- Report a knowledge base that exists but failed to archive as failed, with the
reason, rather than as not found. The copilot delete loop folded every
non-not-found failure into `notFound`, telling the user it was never there.
- Route copilot failures through the same message helper the HTTP surfaces use,
so an unclassified fault's raw text (a driver's failed SQL) no longer reaches
the agent verbatim while the UI and public APIs get the generic wording.
* feat(api): expand the public v2 files surface (#6160)
* feat(api): expand the public v2 files surface
Adds folder support, rename/restore, move, bulk archive, share, and content
replace to /api/v2/files, so managing files by API no longer stops at
upload + download + archive-one.
Routes are thin: auth -> parse -> perform* -> serialize. Share and content
replace get their orchestration extracted first so the session routes and
the public ones cannot diverge on the effective-authType resolution, the
EE public-sharing gate, or the storage-quota classification.
Presigned upload stays session-only: presign does an advisory quota check
and the real debit happens in the separate register step, so a caller that
never registers leaves unaccounted bytes with no reaper. The buffered
multipart path debits inside uploadWorkspaceFile's own transaction.
* fix(files): classify folder and content failures instead of 500ing them
Bugbot round 1. The v2 routes map errorCode straight to a status, so every
manager failure that arrived unclassified became a 500 for what is really a
caller-fixable 400 or 404.
- Folder manager throws OrchestrationError: missing target/folder -> not_found,
reparent cycle / self-parent / restore-into-archived-workspace -> validation.
- File manager does the same for the in-transaction 'File not found' paths that
the earlier pass missed.
- updateWorkspaceFileContent's outer catch re-wrapped everything in a bare
Error, which stripped the class off StorageLimitExceededError and the new
not_found alike. It now rethrows a classified failure untouched and attaches
cause to the generic wrap, so asOrchestrationError can still walk the chain.
- Every remaining perform* gained the asOrchestrationError branch.
- renameWorkspaceFile returned the pre-update read, so the v2 PATCH reported a
stale updatedAt; it now returns the timestamp it actually wrote.
Docs: upload auto-suffixes a duplicate name rather than rejecting it, matching
the in-app uploader. The description claimed 409 and was simply wrong.
* fix(files): surface a failed upload read-back as the real error
getWorkspaceFile swallows a query failure and returns null unless throwOnError
is set, so a transient blip on the post-upload read reported as 'file could not
be read back'. Distinguish the two: a real null after a just-committed write is
an invariant break, a query failure is itself.
* revert(api): drop the dedicated v2 file-folder routes
File folders already live in the shared folder table as resourceType 'file'
(#6045 cut them over, #6051 dropped workspace_file_folders), and the remaining
file-specific folder machinery is being folded into the generic folder engine.
Publishing /api/v2/files/folders/** would pin that transitional split into a
public contract we'd then have to keep or break.
Files stay folder-aware — folderId/folderPath on the projection, folderId on
upload, and the move route — because a folder id is a folder.id and survives
the unification untouched. Folder management belongs on /api/v2/folders once
that surface serves resourceType 'file'; until then there is no v2 way to
enumerate file folders, which is the deliberate gap.
The orchestration classification fixes stay: the internal routes and the
copilot file-folder tools still call those perform* functions.
* fix(files): classify upload failures instead of matching their wording
Bugbot round 2. uploadWorkspaceFile had the same outer-catch rewrap that
updateWorkspaceFileContent did, so a blown storage quota reached the route as a
bare Error and the v2 handler recovered the status by substring-matching the
message. Any rewording silently demoted a 413 to a 500.
- uploadWorkspaceFile rethrows a classified failure untouched and attaches cause
to the generic wrap.
- FileConflictError is now an OrchestrationError('conflict'), so a duplicate name
classifies like every other conflict. Its 'FILE_EXISTS' discriminator had no
readers and is gone; the instanceof checks elsewhere still hold.
- The v2 upload handler uses v2CaughtOrchestrationError, dropping all three
string matches.
Also documents that bulk-archive is best-effort: unknown or already-archived ids
are skipped rather than failing the call, and deletedItems is what actually
happened. That asymmetry with the single-id DELETE was undocumented.
* feat(api): add search, filtering, and sorting to the v2 list endpoints (#6189)
* feat(api): add search, filtering, and sorting to the v2 list endpoints
One convention across every v2 list, documented on lib/api/contracts/v2/shared.ts:
`search` (case-insensitive substring on the resource's natural name field),
`sortBy` + `sortOrder` (per-resource enum, never a free string), and enumerated
resource-specific filters. Reuses the sortBy/sortOrder pair v2 logs and v2
knowledge-documents already ship rather than inventing a third dialect
alongside the Logs filters and the Tables predicate grammar.
Every filter and sort is pushed into SQL. GET /api/v2/files previously read the
whole scope and sorted/sliced it in JS; it now goes through a new
queryWorkspaceFiles that filters, orders, and bounds the page in one query.
Cursors are stamped with the sort they were minted under, so replaying one
under a different sort is a 400 instead of silently duplicated or skipped rows.
* fix(api): validate v2 cursor key values and compare timestamps at ms precision
Two review findings, fixed at the root by making a keyset key own its cursor
codec instead of hand-writing a decoder per sort.
Cursor key values are caller-controlled, and matching the sort stamp and key
count was not enough: an unparseable timestamp or a non-numeric size reached
the query as an Invalid Date or NaN and surfaced as a 500. Each key now type-
checks its own value and rejects a cursor it cannot hold, which both routes
render as the documented 400.
Timestamp keys now order and compare on date_trunc('milliseconds', col).
Postgres keeps microseconds and defaultNow() populates them, but a cursor value
round-trips through a millisecond-only JS Date — comparing the raw column
against the truncated value re-admitted the page's own last row, duplicating it
and stalling pagination outright at a page size of one. Reachable today via
workspace_files.updated_at, which insertFileMetadata leaves to defaultNow().
* feat(api): complete the v2 workflows resource with versions and CRUD (#6184)
* feat(api): complete the v2 workflows resource with versions and CRUD
Adds version listing/detail plus create, update, and delete to the v2
workflows surface, which previously covered only execution and deployment.
- GET /api/v2/workflows/[id]/versions — cursor-paginated, newest first
- GET /api/v2/workflows/[id]/versions/[version] — version + pinned state
- POST /api/v2/workflows, PATCH and DELETE /api/v2/workflows/[id]
All six delegate to the existing orchestration and persistence helpers;
no new domain logic.
* fix(api): check folder containment before lock state; reject malformed version cursors
assertFolderMutable walks a folder's ancestor chain without filtering on
workspace, so inspecting it before containment let a caller tell a locked
folder in someone else's workspace (423) from a nonexistent one (400).
Create and update now assert containment first, matching the ordering
import-workflow.ts already uses.
A version cursor that decodes to JSON without a numeric version filtered
every row out and returned an empty page with nextCursor null, which reads
as a clean end-of-list. Malformed cursors are now a 400.
* refactor(api): page workflow versions in the persistence helper
listWorkflowVersions read every version row and the route filtered and
sliced the result in memory, so the response was bounded but the query
was not. It now takes optional limit/afterVersion, turning the cursor
into a real keyset query; the route asks for limit + 1 and only trims
the has-more probe. Both params are optional, so the internal, v1 admin,
and copilot callers are unchanged.
Also restores the untouched GET handler in [id]/route.ts to its original
formatting — collapsing its signature had re-indented the whole body and
buried the actual additions in whitespace churn.
* feat(api): expand v2 tables with stateless multipart transfers (#6188)
* feat(api): expand the public v2 tables surface
Adds 16 operations so a v2 caller can do what the internal surface can:
rename/move/lock a table, restore it, manage saved views, run enrichment
columns, look up rows, and import/export with observable job control.
Extracts lib/table/orchestration/import.ts (performTableCsvImport,
performCreateTableFromCsv) and lib/table/export-stream.ts from the
first-party routes, then repoints those routes at them, so v1 and v2
cannot drift on what an import or export actually does.
events/stream, metadata and dispatches stay internal — they are editor
state, not public API.
* fix(api): make v2 table PATCH all-or-nothing and name the lock in every 423
Greptile P1: PATCH applied locks, rename and move as three sequential
transactions, so a folder rejected mid-request left the earlier writes
persisted while the response reported failure — and the schema-changed
signal was skipped, leaving open clients on stale state. Every rejectable
condition now runs before the first write, and the signal fires whenever
anything did land.
Cursor: v2TableLockError dropped the lock kind, so async import, column
run, enrichment and table mutations returned a bare LOCKED. A table has
four independent locks, so the caller could not tell which to clear.
* fix(api): report the lock kind on classified 423s too, not just thrown ones
The previous commit named the lock only where the rejection was thrown and
caught at the route boundary. Where it instead arrives as a classified
`errorCode: 'locked'` outcome — delete table, delete row, update column,
and the table mutations — the kind was dropped, so those 423s stayed
unactionable while their neighbours improved.
The orchestration results now carry `lock`, and a shared
`v2TableOrchestrationError` renders both arrival paths into the same
`{ code, message, details: { lock } }` body. `details` is omitted rather
than sent null when the kind is unknown, so a caller branching on it sees
absence instead of a phantom value.
* fix(api): make async table imports observable, not just startable
`POST /import-async` pointed callers at `GET /api/v2/tables/jobs` to track
progress, but that endpoint filters to `type = 'export'` — imports are
derived onto the table itself, one write job at a time, and exports get a
separate list precisely because they are excluded from that derivation.
The public Table shape omitted those derived fields, so an async import
could be started and cancelled but never observed to completion, failure,
or progress. That is the gap the import/export/job-control set was meant
to close.
Table now carries `job` — id, type, status, rowsProcessed, error, or null
when idle — and the import-async docs point at the table rather than the
export list.
* feat(api): make v2 table PATCH state which operations landed on failure
Greptile held the PR at 4/5 on the residual non-atomicity and named two
acceptable resolutions: make PATCH atomic, or have the contract adopt and
expose partial-success explicitly. Atomicity would mean threading one
transaction through renameTable, moveTableToFolder and updateTableLocks —
three shared service functions with four non-test callers including the
first-party route and two copilot tools — and deferring their per-operation
audits to commit time. That is a refactor of shared write paths well
outside this PR.
So the contract states it instead. Every rejectable condition is already
pre-validated, so a failure here is a genuine fault; when one follows a
successful operation the error now carries `details.applied` listing what
is live. Absent when nothing applied, so its presence always means "these
changes took effect despite the error". Documented on the operation.
`v2ErrorForOrchestration` gained the optional `details` this needs.
* fix(api): make table lock flags read-only on the public v2 surface
The new PATCH /api/v2/tables/[tableId] accepted a `locks` object, gated
on workspace admin plus the table-locks feature. That still lets an API
key clear the guard placed there to stop it: `write` is the floor for
the endpoint, and admin keys are ordinary API keys, so a lock is no
longer a boundary the key cannot cross.
Locks stay readable on the table resource and enforcement is unchanged
(a locked verb still returns 423). Changing one is now a first-party
admin action only.
The v2 body is declared here rather than reusing the first-party
updateTableBodySchema, which keeps its `locks` field so the UI can still
toggle them. It is .strict(), so a request carrying `locks` is rejected
with a 400 naming the field instead of silently succeeding without
applying it.
* fix(api): keep reporting applied operations when the PATCH re-read fails
The composite table PATCH promises that `error.details.applied` names the
operations that are live despite an error, but `applied` was scoped
inside the try. A rename or move that committed and was then followed by
a throw in the final re-read — or a re-read finding the table archived —
returned a bare 500/404 with no details, telling the caller nothing had
landed. It would then retry into a duplicate-name conflict or repeat the
move.
`applied` is now function-scoped so every post-write exit carries it: the
404 on a missing re-read, a thrown lock error, a classified orchestration
error, and the generic 500. `v2TableLockError` gains the same
`extraDetails` parameter `v2TableOrchestrationError` already had.
* feat(api): add workflow group writes to the v2 tables surface
v2 exposed GET /groups but none of the writes, so the public API could
run an enrichment or workflow column and read its binding, but never
create one. A caller could add a plain data column and trigger the
machine; wiring the two together still required the UI.
Adds POST/PATCH/DELETE on /api/v2/tables/[tableId]/groups. The group is
the unit that fills columns — one group feeds several — so creating one
creates its output columns in the same call, matching the first-party
shape rather than inverting it onto the column endpoint.
Four departures from the first-party body, all public-surface concerns:
- group.id is optional and server-generated. The UI mints an id to render
optimistically; a public caller has no such need and a client-chosen id
is a collision waiting to happen.
- outputColumns[].workflowGroupId is dropped from the body and stamped
from the resolved group, so it cannot disagree with it.
- autoRun defaults to false. First-party defaults true so a UI add fills
cells immediately; here it would make one POST fan out a metered run
across every existing row.
- A group naming neither a workflowId (type manual) nor an enrichmentId
(type enrichment) is a 400 rather than a half-specified group the route
has to guess about.
Also rejects an outputColumns entry no group output feeds — the two
arrays are joined by column name, and the first-party client builds both
from one picker so it cannot desync, but a public caller can.
Workspace containment on workflowId is asserted before it is persisted,
on create and on any update that re-points the group; without it a table
becomes a way to invoke workflows the key cannot otherwise reach.
* improvement(api): make v2 table import and export async-only
Drops the three synchronous entry points: POST /tables/[tableId]/import,
POST /tables/import-csv, and GET /tables/[tableId]/export.
Sync import tied a write to the lifetime of an HTTP request. The body
*was* the data, so it carried a 10 MB cap that Next silently truncates
past — a partial import reporting success. It also had no job, so a
timeout mid-write left rows in place with nothing to poll and nothing to
cancel. The async path reads the file from storage instead: upload via
POST /api/v2/files for a key, start with POST /import-async, watch
GET /tables/[tableId] -> job, stop with POST /job/cancel.
Sync export carried no such hazard, but one shape per operation beats
two: with both removed the surface has exactly one way to move a table
in or out, and the CLI wraps the extra calls.
This also removes the last multipart handling in v2 tables. Those were
the only routes bypassing parseRequest — form fields were parsed by hand
against separate form schemas, outside the contract system every other
v2 write goes through.
Create-a-table-from-CSV is now two calls: POST /tables, then
/import-async with createColumns. csvImportModeSchema is append|replace,
so there is no single-call create.
Route baseline 1064 -> 1061.
* docs(api): correct the import-async note about upload size limits
The docstring claimed there is no synchronous upload endpoint and so no
request-body size cliff. Both are wrong: POST /api/v2/files is a
synchronous multipart upload with a 100 MB cap, and it is the only v2
upload path (presigned is deliberately absent).
What async-only actually bought: the cap went 10 MB -> 100 MB, it fails
on an explicit size check and a bounded body read rather than a proxy cap
that silently truncates, authorization completes before any body is
buffered, and the table write is a job that can be watched and cancelled.
* feat(api): unify file and table transfers
* improvement(api): make multipart transfers stateless
* fix(api): make table import completion retries idempotent
* feat(v2-tables): paginate the table list
`GET /api/v2/tables` returned every table in the workspace in one response —
it used the cursor envelope but hardcoded `nextCursor: null`, and had no
`limit`. That was defensible when tables were only created through the UI;
`POST /api/v2/tables` is public now, so a script can create them in bulk and
the list has no way to ask for less.
Adds `queryTables` alongside `listTables` rather than changing it, so the
internal callers that genuinely want the whole scope are untouched — the same
split `queryWorkspaceFiles` / `listWorkspaceFiles` already uses. Filter, order
and slice all run in the query, so a `search` never costs a full-workspace read.
A cursor whose values don't bind raises a validation error instead of being
coerced to "no filter", which would have silently served page 1 under a resumed
cursor. The keyset closes on `id` so a page boundary inside a run of equal names
or timestamps stays stable.
The shared `LimitQuery` doc component said "Maximum rows to return"; it now
serves the table list too, so the wording is resource-neutral.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(api): add multipart knowledge document uploads
* fix(api): keep usage admission at knowledge upload session creation
* feat(knowledge): wire knowledge base uploads to multipart sessions
* fix(knowledge): refuse to abort an upload once a document is bound
* fix(uploads): prevent multipart cleanup races
* Unify file creation and signed upload sessions (#6264)
* feat(uploads): unify signed upload sessions
* fix(uploads): preserve attachment storage semantics
* feat(files): add authored file creation
* fix(uploads): omit hoisted S3 metadata headers
* feat(api): add file metadata endpoint
* improvement(api): scope folders to resource paths (#6284)
* improvement(api): scope folders to resource paths
* fix(files): serialize folder resolution with uploads
* fix(files): release folder lock before upload setup
* fix(api): normalize folder paths and unblock resource mutations
* fix(api): make resource cleanup and metadata consistent
* improvement(uploads): persist multipart sessions in postgres
* fix(db): store table row trigger timestamps in UTC
* improvement(api): default folder deletion to non-recursive
* fix(billing): unify chat usage source
* improvement(logs): expose trace spans on log detail
* fix(logs): parse list trace spans
* improvement(api): replace workflow jobs with execution resources (#6294)
* improvement(api): replace workflow jobs with execution resources
* fix(api): preserve legacy jobs while preferring v2 executions
* fix(api): make execution polling resume-aware
* fix(ui): hide async examples for public workflows
* fix(api): bridge resume queue visibility lag
* feat(api): add v2 workflow resume endpoint
* fix(api): project pending resume attempts
* fix(api): prefer terminal logs over stale resumes
* improvement(api): unify v2 resource query layers (#6319)
* improvement(api): unify v2 resource query layers
* fix(api): address v2 review findings
* fix(api): preserve cancelled queue status
* fix(api): guard cancelled job transitions
* fix(api): close v2 resume and log gaps
* feat(api): rename v2 executions to runs
* feat(api): split credentials and secrets
* feat(api): add workspace metadata and email attribution
* improvement(api): consolidate public v2 route handling
* improvement(files): centralize operations across APIs and Copilot (#6392)
* improvement(files): unify rename authorization
* chore(skills): add file operation migration guide
* improvement(files): consolidate file operation authorization
* improvement(files): extract shared operation foundation
* improvement(api): simplify internal route declarations
* improvement(files): centralize application authorization
* refactor(api): share workspace file name validation
* refactor(files): centralize copilot application calls
* docs(skills): generalize application operation migration
* improvement(api): centralize remaining v2 resource operations (#6412)
* improvement(api): centralize v2 resource operations
* fix(api): preserve custom tool conflict errors
* improvement(api): migrate policy-sensitive v2 reads (#6410)
* improvement(workflows): centralize v2 application operations (#6411)
* refactor(api): migrate v2 knowledge operations (#6413)
* refactor(api): migrate v2 knowledge operations
* fix(knowledge): fail upload completion on dispatch errors
* fix(knowledge): preserve upload retry and VFS errors
* improvement(tables): centralize v2 application operations (#6414)
* improvement(tables): centralize v2 application operations
* fix(tables): preserve run validation and signals
* feat(auth): add scoped internal executor delegation (#6459)
* feat(auth): add scoped internal executor delegation
* fix(auth): derive delegation lifetime from one timestamp
* Include share status in file metadata
* feat(auth): centralize delegated identity policy (#6462)
* improvement(copilot): consolidate application adapters (#6450)
* improvement(api): harden application route boundaries (#6451)
* improvement(api): harden application route boundaries
* fix(folders): reject creates at workspace cap
* fix(knowledge): enforce trusted workspace scope (#6452)
* fix(knowledge): enforce trusted workspace scope
* refactor(knowledge): declare v2 body lifecycle
* finish knowledge application migration
* refactor(knowledge): compose copilot batch commands
* fix(knowledge): parse connector query flags
* fix(knowledge): finalize partial batch effects
* fix(knowledge): align merged application boundaries
* fix(knowledge): close application boundary review gaps
* style(knowledge): satisfy branch biome checks
* fix(knowledge): page connector documents in editor
* refactor: enforce Copilot table application boundary (#6453)
* refactor: enforce copilot table application boundary
* fix(tables): finish application boundary migration
* fix(tables): restore scoped copilot imports
* fix(tables): compose copilot commands atomically
* fix(tables): preserve workflow group scheduling
* fix(tables): complete fixed copilot composition
* fix(tables): reject enrichment output mutation
* fix(tables): complete authorized application boundary
* fix(workflows): migrate Copilot application boundary (#6455)
* fix(workflows): migrate Copilot application boundary
* fix(workflows): finish delegated application migration
* fix(workflows): encode VFS folder aliases
* fix(workflows): close application composition gaps
* fix(workflows): preserve VFS validation errors
* fix(workflows): complete application boundary migration
* test(workflows): format canonical binding coverage
* fix(workflows): scope executor metadata reads
* fix(workflows): bind executor metadata targets
* improvement(skills): align application operation guidance (#6532)
* feat(api): expose v2 resource owners
* fix(api): distinguish visible resource authorization failures (#6537)
* feat(api): generate v2 OpenAPI from contracts (#6509)
* feat(api): generate v2 OpenAPI from contracts
* fix(api): preserve string boolean wire defaults
* fix(api): document file download headers
* fix(docs): use TypeScript CLI with Next.js
* fix(docs): avoid client-rendered theme script
* fix(api): document departed audit default
* feat(api): replace legacy core docs with v2
* feat(api): generate v2 OpenAPI from contracts
* feat(api): refine generated v2 OpenAPI docs
* fix(docs): align localized v2 execution examples
* fix(ci): restore Helm diff and sync audit mock
* fix CI regressions after staging merge
---------
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Theodore Li <theodoreqili@gmail.com>
Co-authored-by: Siddharth Ganesan <33737564+Sg312@users.noreply.github.com>
Co-authored-by: Theodore Li <theo@sim.ai>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@TheodoreSpeaks