feat(code): cli sandboxes, enterprise timeouts, secrets projections, resolver lift, workflow exec cancellations (#6247)

* feat(code): cli sandboxes, enterprise timeouts, secrets projections, resolver lift

* fix(execution): harden compatibility and secret diagnostics

* fix(execution): harden generated JavaScript literals

* fix(execution): align timeout cleanup semantics

* fix(tables): decouple stale job cleanup

* fix(execution): drain stale workflow backlog

* test(sandbox): make deadline assertions timing-safe

* fix(execution): lock cleanup candidate batches

* fix(execution): preserve cleanup failure metrics

* cancel route fixes

* separate out mship template and func template

* fix

* fix(execution): harden secret projection and block runs

* fix(workflow): validate draft execution state

* run from block ui disabling

* feat(copilot): expose Sim sandboxes to mothership

* feat(copilot): expose sandbox capability catalog in VFS

* Updates

* fix legacy logs showing up

* fix(copilot): keep sandbox config visible

* fix model provenance issues

* fix lint'

* more lint

* more

* test(files): align provenance copy query order

* consolidate migrations, rollout compat

* integration projections

* update skills

* fix

* add provenance linters

* fix: address review and compatibility regressions

* fix: make tool boundary audit Bun 1.3 compatible

---------

Co-authored-by: Siddharth Ganesan <siddharthganesan@gmail.com>
This commit is contained in:
Vikhyath Mondreti
2026-08-05 19:22:04 -07:00
committed by GitHub
co-authored by Siddharth Ganesan
parent 5baa7a41ec
commit 117fe3137b
826 changed files with 95636 additions and 7877 deletions
@@ -0,0 +1,124 @@
-- Replay-safety: this file ends in a concurrent index build after an embedded COMMIT, so a
-- failure there replays the preceding schema work. Every statement before and after the COMMIT
-- is therefore idempotent.
-- migration-safe: all application columns are additive; existing durable rows retain NULL as
-- their legacy provenance marker, and the new private sidecar tables do not rewrite user data.
CREATE TABLE IF NOT EXISTS "document_secret_provenance" (
"document_id" text PRIMARY KEY NOT NULL,
"source_hash" text NOT NULL,
"status" text NOT NULL,
"entries" jsonb DEFAULT '[]'::jsonb NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "document_secret_provenance_status_check" CHECK ("document_secret_provenance"."status" IN ('exact', 'unknown')),
CONSTRAINT "document_secret_provenance_document_id_document_id_fk" FOREIGN KEY ("document_id") REFERENCES "public"."document"("id") ON DELETE cascade ON UPDATE no action
);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "embedding_secret_provenance" (
"embedding_id" text PRIMARY KEY NOT NULL,
"content_hash" text NOT NULL,
"status" text NOT NULL,
"entries" jsonb DEFAULT '[]'::jsonb NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "embedding_secret_provenance_status_check" CHECK ("embedding_secret_provenance"."status" IN ('exact', 'unknown')),
CONSTRAINT "embedding_secret_provenance_embedding_id_embedding_id_fk" FOREIGN KEY ("embedding_id") REFERENCES "public"."embedding"("id") ON DELETE cascade ON UPDATE no action
);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "memory_secret_provenance" (
"memory_id" text PRIMARY KEY NOT NULL,
"content_hash" text NOT NULL,
"status" text NOT NULL,
"entries" jsonb DEFAULT '[]'::jsonb NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "memory_secret_provenance_status_check" CHECK ("memory_secret_provenance"."status" IN ('exact', 'unknown')),
CONSTRAINT "memory_secret_provenance_memory_id_memory_id_fk" FOREIGN KEY ("memory_id") REFERENCES "public"."memory"("id") ON DELETE cascade ON UPDATE no action
);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "user_table_row_secret_provenance" (
"row_id" text PRIMARY KEY NOT NULL,
"content_updated_at" timestamp NOT NULL,
"status" text NOT NULL,
"entries" jsonb DEFAULT '[]'::jsonb NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "user_table_row_secret_provenance_status_check" CHECK ("user_table_row_secret_provenance"."status" IN ('exact', 'unknown')),
CONSTRAINT "user_table_row_secret_provenance_row_id_user_table_rows_id_fk" FOREIGN KEY ("row_id") REFERENCES "public"."user_table_rows"("id") ON DELETE cascade ON UPDATE no action
);--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "workspace_file_secret_provenance" (
"file_id" text PRIMARY KEY NOT NULL,
"content_updated_at" timestamp NOT NULL,
"status" text NOT NULL,
"entries" jsonb DEFAULT '[]'::jsonb NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "workspace_file_secret_provenance_status_check" CHECK ("workspace_file_secret_provenance"."status" IN ('exact', 'unknown')),
CONSTRAINT "workspace_file_secret_provenance_file_id_workspace_files_id_fk" FOREIGN KEY ("file_id") REFERENCES "public"."workspace_files"("id") ON DELETE cascade ON UPDATE no action
);--> statement-breakpoint
ALTER TABLE "document" ADD COLUMN IF NOT EXISTS "secret_provenance_version" integer;--> statement-breakpoint
ALTER TABLE "embedding" ADD COLUMN IF NOT EXISTS "secret_provenance_version" integer;--> statement-breakpoint
ALTER TABLE "memory" ADD COLUMN IF NOT EXISTS "secret_provenance_version" integer;--> statement-breakpoint
ALTER TABLE "sandbox_image" ADD COLUMN IF NOT EXISTS "materialization_generation" bigint;--> statement-breakpoint
ALTER TABLE "user_table_rows" ADD COLUMN IF NOT EXISTS "secret_provenance_version" integer;--> statement-breakpoint
ALTER TABLE "workflow_execution_logs" ADD COLUMN IF NOT EXISTS "execution_deadline_at" timestamp;--> statement-breakpoint
ALTER TABLE "workspace_files" ADD COLUMN IF NOT EXISTS "secret_provenance_version" integer;--> statement-breakpoint
ALTER TABLE "workspace_sandbox" ADD COLUMN IF NOT EXISTS "cli_tools" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
ALTER TABLE "workspace_sandbox" ADD COLUMN IF NOT EXISTS "system_packages" jsonb DEFAULT '[]'::jsonb NOT NULL;--> statement-breakpoint
-- During a rolling deploy, a legacy application process can still mutate durable content without
-- updating the new sidecar. Demoting the marker makes the new application use the established
-- legacy-read behavior instead of trusting a stale sidecar. Provenance-aware writers restore
-- version 1 only after writing the matching sidecar in the same transaction.
CREATE OR REPLACE FUNCTION "demote_secret_provenance_version"()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW."secret_provenance_version" := NULL;
RETURN NEW;
END;
$$;--> statement-breakpoint
CREATE OR REPLACE FUNCTION "demote_user_table_row_secret_provenance"()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW."secret_provenance_version" := NULL;
NEW."updated_at" := GREATEST(
date_trunc('milliseconds', NEW."updated_at"),
date_trunc('milliseconds', OLD."updated_at") + interval '1 millisecond'
);
RETURN NEW;
END;
$$;--> statement-breakpoint
DROP TRIGGER IF EXISTS "document_secret_provenance_demote" ON "document";--> statement-breakpoint
CREATE TRIGGER "document_secret_provenance_demote"
BEFORE UPDATE OF "filename", "file_url", "content_hash", "source_url", "tag1", "tag2", "tag3", "tag4", "tag5", "tag6", "tag7", "number1", "number2", "number3", "number4", "number5", "date1", "date2", "boolean1", "boolean2", "boolean3" ON "document"
FOR EACH ROW
WHEN (ROW(OLD."filename", OLD."file_url", OLD."content_hash", OLD."source_url", OLD."tag1", OLD."tag2", OLD."tag3", OLD."tag4", OLD."tag5", OLD."tag6", OLD."tag7", OLD."number1", OLD."number2", OLD."number3", OLD."number4", OLD."number5", OLD."date1", OLD."date2", OLD."boolean1", OLD."boolean2", OLD."boolean3") IS DISTINCT FROM ROW(NEW."filename", NEW."file_url", NEW."content_hash", NEW."source_url", NEW."tag1", NEW."tag2", NEW."tag3", NEW."tag4", NEW."tag5", NEW."tag6", NEW."tag7", NEW."number1", NEW."number2", NEW."number3", NEW."number4", NEW."number5", NEW."date1", NEW."date2", NEW."boolean1", NEW."boolean2", NEW."boolean3"))
EXECUTE FUNCTION "demote_secret_provenance_version"();--> statement-breakpoint
DROP TRIGGER IF EXISTS "embedding_secret_provenance_demote" ON "embedding";--> statement-breakpoint
CREATE TRIGGER "embedding_secret_provenance_demote"
BEFORE UPDATE OF "content", "chunk_hash" ON "embedding"
FOR EACH ROW
WHEN (ROW(OLD."content", OLD."chunk_hash") IS DISTINCT FROM ROW(NEW."content", NEW."chunk_hash"))
EXECUTE FUNCTION "demote_secret_provenance_version"();--> statement-breakpoint
DROP TRIGGER IF EXISTS "memory_secret_provenance_demote" ON "memory";--> statement-breakpoint
CREATE TRIGGER "memory_secret_provenance_demote"
BEFORE UPDATE OF "data" ON "memory"
FOR EACH ROW
WHEN (OLD."data" IS DISTINCT FROM NEW."data")
EXECUTE FUNCTION "demote_secret_provenance_version"();--> statement-breakpoint
DROP TRIGGER IF EXISTS "user_table_rows_secret_provenance_demote" ON "user_table_rows";--> statement-breakpoint
CREATE TRIGGER "user_table_rows_secret_provenance_demote"
BEFORE UPDATE OF "data" ON "user_table_rows"
FOR EACH ROW
WHEN (OLD."data" IS DISTINCT FROM NEW."data")
EXECUTE FUNCTION "demote_user_table_row_secret_provenance"();--> statement-breakpoint
DROP TRIGGER IF EXISTS "workspace_files_secret_provenance_demote" ON "workspace_files";--> statement-breakpoint
CREATE TRIGGER "workspace_files_secret_provenance_demote"
BEFORE UPDATE OF "content_updated_at" ON "workspace_files"
FOR EACH ROW
WHEN (OLD."content_updated_at" IS DISTINCT FROM NEW."content_updated_at")
EXECUTE FUNCTION "demote_secret_provenance_version"();--> statement-breakpoint
-- The execution-log table is live and can be large. End the migration transaction before the
-- index build so writes remain available. A failed build leaves this migration unjournaled; the
-- replay-safe drop removes any INVALID same-name index before rebuilding it.
COMMIT;--> statement-breakpoint
SET lock_timeout = 0;--> statement-breakpoint
DROP INDEX CONCURRENTLY IF EXISTS "workflow_execution_logs_running_deadline_idx";--> statement-breakpoint
CREATE INDEX CONCURRENTLY IF NOT EXISTS "workflow_execution_logs_running_deadline_idx" ON "workflow_execution_logs" USING btree ("execution_deadline_at") WHERE "workflow_execution_logs"."status" = 'running' AND "workflow_execution_logs"."execution_deadline_at" IS NOT NULL;--> statement-breakpoint
SET lock_timeout = '5s';
File diff suppressed because it is too large Load Diff
@@ -1975,6 +1975,13 @@
"when": 1785968181949,
"tag": "0282_chubby_psylocke",
"breakpoints": true
},
{
"idx": 283,
"version": "7",
"when": 1785978020563,
"tag": "0283_military_fabian_cortez",
"breakpoints": true
}
]
}
+152
View File
@@ -412,6 +412,8 @@ export const workflowExecutionLogs = pgTable(
trigger: text('trigger').notNull(), // 'api' | 'webhook' | 'schedule' | 'manual' | 'chat'
startedAt: timestamp('started_at').notNull(),
/** Absolute deadline for the current active attempt; cleared while paused or terminal. */
executionDeadlineAt: timestamp('execution_deadline_at'),
endedAt: timestamp('ended_at'),
totalDurationMs: integer('total_duration_ms'),
@@ -475,6 +477,9 @@ export const workflowExecutionLogs = pgTable(
runningStartedAtIdx: index('workflow_execution_logs_running_started_at_idx')
.on(table.startedAt)
.where(sql`status = 'running'`),
runningExecutionDeadlineIdx: index('workflow_execution_logs_running_deadline_idx')
.on(table.executionDeadlineAt)
.where(sql`${table.status} = 'running' AND ${table.executionDeadlineAt} IS NOT NULL`),
completedEndedAtIdx: index('workflow_execution_logs_completed_ended_at_idx')
.on(table.endedAt, table.workspaceId, table.executionId)
.where(
@@ -1936,6 +1941,13 @@ export const workspaceFiles = pgTable(
* is covered without per-call plumbing. Only a content write (upload / overwrite) advances it.
*/
contentUpdatedAt: timestamp('content_updated_at').notNull().defaultNow(),
/**
* Durable cutover marker for content secret provenance. NULL is reserved for legacy rows and
* writes from app versions that predate tracking. Provenance-aware writers set version 1 in the
* same transaction as the matching sidecar. A tracked version without a matching sidecar fails
* closed.
*/
secretProvenanceVersion: integer('secret_provenance_version'),
},
(table) => ({
keyActiveUniqueIdx: uniqueIndex('workspace_files_key_active_unique')
@@ -1972,6 +1984,51 @@ export const workspaceFiles = pgTable(
})
)
export interface WorkspaceFileSecretProvenanceEntry extends DurableSecretProvenanceEntry {
name: string
sourceUserId: string
}
/**
* Private, durable provenance for bytes stored in `workspace_files`.
*
* Absence is reserved for legacy files that predate provenance tracking. `exact` records carry the
* encrypted values found in one content version (including an empty set); `unknown` records fail
* closed at model attachment boundaries. Keeping this one-to-one state outside `workspace_files`
* prevents private metadata from leaking through broad workspace-file record projections.
*/
export const workspaceFileSecretProvenance = pgTable(
'workspace_file_secret_provenance',
{
fileId: text('file_id')
.primaryKey()
.references(() => workspaceFiles.id, { onDelete: 'cascade' }),
contentUpdatedAt: timestamp('content_updated_at').notNull(),
status: text('status').notNull(),
entries: jsonb('entries').$type<WorkspaceFileSecretProvenanceEntry[]>().notNull().default([]),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
statusCheck: check(
'workspace_file_secret_provenance_status_check',
sql`${table.status} IN ('exact', 'unknown')`
),
})
)
export interface DurableSecretProvenanceEntry {
encryptedValue: string
name?: string
sourceUserId?: string
sourceWorkspaceId?: string
/** Optional canonical hash of the exact persisted sub-value that contributed this entry. */
sourceValueHash?: string
}
export interface TableRowSecretProvenanceEntry extends DurableSecretProvenanceEntry {
columnId: string
}
/**
* Cached collaborative-document state for a workspace markdown file: the last-persisted Yjs binary and
* a hash of the markdown it was derived from. On a cold room open the seed loads this binary directly
@@ -2122,6 +2179,8 @@ export const memory = pgTable(
.references(() => workspace.id, { onDelete: 'cascade' }),
key: text('key').notNull(),
data: jsonb('data').notNull(),
/** NULL is a legacy/untracked record; version 1 requires a fresh private sidecar. */
secretProvenanceVersion: integer('secret_provenance_version'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
deletedAt: timestamp('deleted_at'),
@@ -2141,6 +2200,26 @@ export const memory = pgTable(
}
)
/** Private provenance bound to one exact canonical hash of the persisted memory data. */
export const memorySecretProvenance = pgTable(
'memory_secret_provenance',
{
memoryId: text('memory_id')
.primaryKey()
.references(() => memory.id, { onDelete: 'cascade' }),
contentHash: text('content_hash').notNull(),
status: text('status').notNull(),
entries: jsonb('entries').$type<DurableSecretProvenanceEntry[]>().notNull().default([]),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
statusCheck: check(
'memory_secret_provenance_status_check',
sql`${table.status} IN ('exact', 'unknown')`
),
})
)
export const knowledgeBase = pgTable(
'knowledge_base',
{
@@ -2256,6 +2335,8 @@ export const document = pgTable(
externalId: text('external_id'),
contentHash: text('content_hash'),
sourceUrl: text('source_url'),
/** NULL is a legacy/untracked source; version 1 requires a matching source sidecar. */
secretProvenanceVersion: integer('secret_provenance_version'),
/** User who uploaded the document, for usage attribution. Null for
* connector/cron-synced docs (and pre-migration rows) → indexing billing
@@ -2315,6 +2396,26 @@ export const document = pgTable(
})
)
/** Private provenance for a document ingestion source, bound by a deterministic source hash. */
export const documentSecretProvenance = pgTable(
'document_secret_provenance',
{
documentId: text('document_id')
.primaryKey()
.references(() => document.id, { onDelete: 'cascade' }),
sourceHash: text('source_hash').notNull(),
status: text('status').notNull(),
entries: jsonb('entries').$type<DurableSecretProvenanceEntry[]>().notNull().default([]),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
statusCheck: check(
'document_secret_provenance_status_check',
sql`${table.status} IN ('exact', 'unknown')`
),
})
)
export const knowledgeBaseTagDefinitions = pgTable(
'knowledge_base_tag_definitions',
{
@@ -2361,6 +2462,8 @@ export const embedding = pgTable(
chunkIndex: integer('chunk_index').notNull(),
chunkHash: text('chunk_hash').notNull(),
content: text('content').notNull(),
/** NULL is a legacy/untracked chunk; version 1 requires a fresh private sidecar. */
secretProvenanceVersion: integer('secret_provenance_version'),
contentLength: integer('content_length').notNull(),
tokenCount: integer('token_count').notNull(),
@@ -2462,6 +2565,26 @@ export const embedding = pgTable(
})
)
/** Private provenance bound to one exact SHA-256 hash of the persisted chunk content. */
export const embeddingSecretProvenance = pgTable(
'embedding_secret_provenance',
{
embeddingId: text('embedding_id')
.primaryKey()
.references(() => embedding.id, { onDelete: 'cascade' }),
contentHash: text('content_hash').notNull(),
status: text('status').notNull(),
entries: jsonb('entries').$type<DurableSecretProvenanceEntry[]>().notNull().default([]),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
statusCheck: check(
'embedding_secret_provenance_status_check',
sql`${table.status} IN ('exact', 'unknown')`
),
})
)
export const docsEmbeddings = pgTable(
'docs_embeddings',
{
@@ -3894,6 +4017,7 @@ export const userTableRows = pgTable(
* express column collation, so the collation lives only in the migration.
*/
orderKey: text('order_key'),
secretProvenanceVersion: integer('secret_provenance_version'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }),
@@ -3932,6 +4056,30 @@ export const userTableRows = pgTable(
})
)
/**
* Encrypted secret provenance for a table row's current JSONB payload.
* The sidecar is bound to `user_table_rows.updated_at`; a missing or stale
* sidecar on a tracked row is treated as unknown at model re-entry.
*/
export const userTableRowSecretProvenance = pgTable(
'user_table_row_secret_provenance',
{
rowId: text('row_id')
.primaryKey()
.references(() => userTableRows.id, { onDelete: 'cascade' }),
contentUpdatedAt: timestamp('content_updated_at').notNull(),
status: text('status').notNull(),
entries: jsonb('entries').$type<TableRowSecretProvenanceEntry[]>().notNull().default([]),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
},
(table) => ({
statusCheck: check(
'user_table_row_secret_provenance_status_check',
sql`${table.status} IN ('exact', 'unknown')`
),
})
)
/**
* Saved presets for a user-defined table — a named filter + sort + column layout.
* Workspace-shared: anyone who can read the table sees every view, and `write` is
@@ -4364,6 +4512,8 @@ export const workspaceSandbox = pgTable(
name: text('name').notNull(),
language: sandboxLanguageEnum('language').notNull(),
dependencies: jsonb('dependencies').$type<string[]>().notNull().default(sql`'[]'::jsonb`),
cliTools: jsonb('cli_tools').$type<string[]>().notNull().default(sql`'[]'::jsonb`),
systemPackages: jsonb('system_packages').$type<string[]>().notNull().default(sql`'[]'::jsonb`),
specHash: text('spec_hash').notNull(),
createdBy: text('created_by').references(() => user.id, { onDelete: 'set null' }),
createdAt: timestamp('created_at').notNull().defaultNow(),
@@ -4397,6 +4547,8 @@ export const sandboxImage = pgTable(
/** Provider-side image identifier, when it differs from `imageRef`. */
providerImageId: text('provider_image_id'),
buildId: text('build_id'),
/** Monotonic target release for this provider materialization; legacy rows are generation 0. */
materializationGeneration: bigint('materialization_generation', { mode: 'number' }),
/** Classified taxonomy code; see lib/execution/remote-sandbox/build-errors.ts. */
errorCode: text('error_code'),
/** User-facing copy rendered from the code at classification time. */
@@ -0,0 +1,40 @@
import { describe, expect, it } from 'vitest'
import { type ChipSelectOption, chipSelectOptionMatchesSearch } from './chip-select'
const GOOGLE_CLOUD_OPTION: ChipSelectOption = {
label: 'Google Cloud CLI',
value: 'google-cloud-cli',
searchTerms: ['gcloud', 'bq', 'gsutil'],
}
describe('chipSelectOptionMatchesSearch', () => {
it('matches labels with normalized casing and whitespace', () => {
expect(chipSelectOptionMatchesSearch(GOOGLE_CLOUD_OPTION, ' CLOUD ')).toBe(true)
})
it.each(['gcloud', 'bq', 'gsu'])('matches the search-only alias %s', (query) => {
expect(chipSelectOptionMatchesSearch(GOOGLE_CLOUD_OPTION, query)).toBe(true)
})
it('normalizes whitespace and casing in search-only aliases', () => {
expect(
chipSelectOptionMatchesSearch(
{ label: 'Google Cloud CLI', value: 'google', searchTerms: [' BIGQUERY '] },
'bigquery'
)
).toBe(true)
})
it('does not expose unrelated options through another option alias', () => {
expect(
chipSelectOptionMatchesSearch(
{ label: 'GitHub CLI', value: 'github', searchTerms: ['gh'] },
'bq'
)
).toBe(false)
})
it('treats an empty normalized query as an unfiltered option list', () => {
expect(chipSelectOptionMatchesSearch(GOOGLE_CLOUD_OPTION, ' ')).toBe(true)
})
})
@@ -18,6 +18,8 @@ import {
export interface ChipSelectOption {
label: string
value: string
/** Additional search-only terms. These are never rendered in the option label. */
searchTerms?: readonly string[]
/** Optional leading icon. */
icon?: React.ComponentType<{ className?: string }>
/** Whether this option is non-selectable. */
@@ -71,6 +73,12 @@ export interface ChipSelectProps {
dropdownWidth?: 'trigger' | number
/** Max height of the menu in px (defaults to the menu's 240px). */
maxHeight?: number
/**
* Keep the menu below its trigger and shrink it to the remaining viewport
* height instead of allowing collision handling to flip it above. Use this
* for long form-field menus that would otherwise obscure preceding fields.
*/
stayBelow?: boolean
/** Forwarded to the trigger button. */
className?: string
/** Forwarded to the menu content. */
@@ -87,6 +95,15 @@ export interface ChipSelectProps {
modal?: boolean
}
/** Matches an option label or one of its search-only aliases. */
export function chipSelectOptionMatchesSearch(option: ChipSelectOption, query: string): boolean {
const normalizedQuery = query.trim().toLowerCase()
if (!normalizedQuery) return true
return [option.label, ...(option.searchTerms ?? [])].some((term) =>
term.trim().toLowerCase().includes(normalizedQuery)
)
}
/**
* The platform filter dropdown: a `filled` chip trigger with a trailing
* chevron that opens a `DropdownMenu`. This is the same pattern the
@@ -126,6 +143,7 @@ export function ChipSelect({
fullWidth = false,
dropdownWidth,
maxHeight,
stayBelow = false,
className,
contentClassName,
'aria-label': ariaLabel,
@@ -157,9 +175,8 @@ export function ChipSelect({
const filteredSections = React.useMemo(() => {
if (!searchable || !query.trim()) return sections
const q = query.toLowerCase()
return sections
.map((g) => ({ ...g, items: g.items.filter((o) => o.label.toLowerCase().includes(q)) }))
.map((g) => ({ ...g, items: g.items.filter((o) => chipSelectOptionMatchesSearch(o, query)) }))
.filter((g) => g.items.length > 0)
}, [searchable, query, sections])
@@ -182,7 +199,12 @@ export function ChipSelect({
if (dropdownWidth === 'trigger') contentStyle.width = 'var(--radix-dropdown-menu-trigger-width)'
else if (typeof dropdownWidth === 'number') contentStyle.width = dropdownWidth
if (dropdownWidth != null) contentStyle.maxWidth = 'none'
if (typeof maxHeight === 'number') contentStyle.maxHeight = maxHeight
if (stayBelow) {
const preferredMaxHeight = typeof maxHeight === 'number' ? `${maxHeight}px` : '240px'
contentStyle.maxHeight = `min(${preferredMaxHeight}, var(--radix-dropdown-menu-content-available-height))`
} else if (typeof maxHeight === 'number') {
contentStyle.maxHeight = maxHeight
}
const renderOption = (opt: ChipSelectOption) => {
const Icon = opt.icon
@@ -246,6 +268,8 @@ export function ChipSelect({
</DropdownMenuTrigger>
<DropdownMenuContent
align={align}
side={stayBelow ? 'bottom' : undefined}
avoidCollisions={stayBelow ? false : undefined}
onOpenAutoFocus={searchable ? (e) => e.preventDefault() : undefined}
style={contentStyle}
className={cn('min-w-[160px]', contentClassName)}
+3 -3
View File
@@ -24,7 +24,7 @@ type PrismModule = typeof import('./prism')
/**
* Module-level singleton promise for the lazily-loaded Prism module.
*
* Prism (core + the side-effectful JS/Python/JSON grammar registrations) is kept
* Prism (core + the side-effectful JS/Python/JSON/Bash grammar registrations) is kept
* out of this module's static import graph so it never lands in bundles that only
* pull `Code` through the shared `@sim/emcn` barrel. It is loaded once per
* session on the first highlight and cached here for all subsequent viewers.
@@ -802,7 +802,7 @@ interface CodeViewerProps {
/** Whether to show line numbers gutter */
showGutter?: boolean
/** Language for syntax highlighting (default: 'json') */
language?: 'javascript' | 'json' | 'python'
language?: 'javascript' | 'json' | 'python' | 'bash'
/** Additional CSS classes for the container */
className?: string
/** Left padding offset (useful for terminal alignment) */
@@ -888,7 +888,7 @@ type ViewerInnerProps = {
/** Whether to show line numbers gutter */
showGutter: boolean
/** Language for syntax highlighting */
language: 'javascript' | 'json' | 'python'
language: 'javascript' | 'json' | 'python' | 'bash'
/** Additional CSS classes for the container */
className?: string
/** Left padding offset in pixels */
@@ -2,6 +2,7 @@ import { type Grammar, languages, highlight as prismHighlight } from 'prismjs'
import 'prismjs/components/prism-javascript'
import 'prismjs/components/prism-python'
import 'prismjs/components/prism-json'
import 'prismjs/components/prism-bash'
/**
* Prism.js highlighting utilities isolated in a dedicated module.
+10 -3
View File
@@ -43,7 +43,7 @@ SimStudioClient(api_key: str, base_url: str = "https://sim.ai")
#### Methods
##### execute_workflow(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None, async_execution=None)
##### execute_workflow(workflow_id, input=None, *, timeout=30.0, stream=None, selected_outputs=None, async_execution=None, execution_timeout_seconds=None)
Execute a workflow with optional input data.
@@ -55,7 +55,13 @@ result = client.execute_workflow("workflow-id", {"message": "Hello, world!"})
result = client.execute_workflow("workflow-id", "NVDA")
# With options (keyword-only arguments)
result = client.execute_workflow("workflow-id", {"message": "Hello"}, timeout=60.0)
result = client.execute_workflow(
"workflow-id",
{"message": "Hello"},
timeout=60.0,
async_execution=True,
execution_timeout_seconds=3600,
)
```
**Parameters:**
@@ -65,6 +71,7 @@ result = client.execute_workflow("workflow-id", {"message": "Hello"}, timeout=60
- `stream` (bool, keyword-only): Enable streaming responses
- `selected_outputs` (list, keyword-only): Block outputs to stream (e.g., `["agent1.content"]`)
- `async_execution` (bool, keyword-only): Execute asynchronously and return execution ID
- `execution_timeout_seconds` (int, keyword-only): Server-side async execution cap from 1 to 604800 seconds. Requires `async_execution=True` and cannot extend the account policy.
**Returns:** `WorkflowExecutionResult` or `AsyncExecutionResult`
@@ -527,4 +534,4 @@ isort simstudio/
## License
Apache-2.0
Apache-2.0
+28 -3
View File
@@ -12,6 +12,7 @@ import os
import requests
MAX_EXECUTION_TIMEOUT_SECONDS = 604_800
__version__ = "0.1.2"
__all__ = [
@@ -155,7 +156,8 @@ class SimStudioClient:
timeout: float = 30.0,
stream: Optional[bool] = None,
selected_outputs: Optional[list] = None,
async_execution: Optional[bool] = None
async_execution: Optional[bool] = None,
execution_timeout_seconds: Optional[int] = None
) -> Union[WorkflowExecutionResult, AsyncExecutionResult]:
"""
Execute a workflow with optional input data.
@@ -172,6 +174,7 @@ class SimStudioClient:
stream: Enable streaming responses (default: None)
selected_outputs: Block outputs to stream (e.g., ["agent1.content"])
async_execution: Execute asynchronously (default: None)
execution_timeout_seconds: Server-side async execution cap in seconds (1-604800)
Returns:
WorkflowExecutionResult or AsyncExecutionResult object
@@ -181,10 +184,29 @@ class SimStudioClient:
"""
url = f"{self.base_url}/api/workflows/{workflow_id}/execute"
if execution_timeout_seconds is not None:
if not async_execution:
raise SimStudioError(
'execution_timeout_seconds is supported only for async executions',
'INVALID_EXECUTION_TIMEOUT'
)
if (
isinstance(execution_timeout_seconds, bool)
or not isinstance(execution_timeout_seconds, int)
or execution_timeout_seconds < 1
or execution_timeout_seconds > MAX_EXECUTION_TIMEOUT_SECONDS
):
raise SimStudioError(
f'execution_timeout_seconds must be an integer between 1 and {MAX_EXECUTION_TIMEOUT_SECONDS}',
'INVALID_EXECUTION_TIMEOUT'
)
# Build headers - async execution uses X-Execution-Mode header
headers = self._session.headers.copy()
if async_execution:
headers['X-Execution-Mode'] = 'async'
if execution_timeout_seconds is not None:
headers['X-Execution-Timeout-Seconds'] = str(execution_timeout_seconds)
try:
# Build JSON body - spread dict inputs at root level, wrap primitives/lists in 'input' field
@@ -421,6 +443,7 @@ class SimStudioClient:
stream: Optional[bool] = None,
selected_outputs: Optional[list] = None,
async_execution: Optional[bool] = None,
execution_timeout_seconds: Optional[int] = None,
max_retries: int = 3,
initial_delay: float = 1.0,
max_delay: float = 30.0,
@@ -436,6 +459,7 @@ class SimStudioClient:
stream: Enable streaming responses
selected_outputs: Block outputs to stream
async_execution: Execute asynchronously
execution_timeout_seconds: Server-side async execution cap in seconds (1-604800)
max_retries: Maximum number of retries (default: 3)
initial_delay: Initial delay in seconds (default: 1.0)
max_delay: Maximum delay in seconds (default: 30.0)
@@ -458,7 +482,8 @@ class SimStudioClient:
timeout=timeout,
stream=stream,
selected_outputs=selected_outputs,
async_execution=async_execution
async_execution=async_execution,
execution_timeout_seconds=execution_timeout_seconds,
)
except SimStudioError as e:
if e.code != 'RATE_LIMIT_EXCEEDED':
@@ -565,4 +590,4 @@ class SimStudioClient:
# For backward compatibility
Client = SimStudioClient
Client = SimStudioClient
+74 -1
View File
@@ -171,6 +171,79 @@ def test_async_header_not_set_when_false(mock_post):
assert "X-Execution-Mode" not in call_args[1]["headers"]
@patch('simstudio.requests.Session.post')
def test_async_execution_timeout_header(mock_post):
mock_response = Mock()
mock_response.ok = True
mock_response.status_code = 202
mock_response.json.return_value = {
"success": True,
"jobId": "job-123",
"statusUrl": "/api/jobs/job-123",
"async": True,
}
mock_response.headers.get.return_value = None
mock_post.return_value = mock_response
client = SimStudioClient(api_key="test-api-key")
client.execute_workflow(
"workflow-id",
{},
async_execution=True,
execution_timeout_seconds=90,
)
headers = mock_post.call_args[1]["headers"]
assert headers["X-Execution-Timeout-Seconds"] == "90"
def test_sync_execution_rejects_execution_timeout():
client = SimStudioClient(api_key="test-api-key")
with pytest.raises(SimStudioError) as exc_info:
client.execute_workflow("workflow-id", {}, execution_timeout_seconds=90)
assert exc_info.value.code == "INVALID_EXECUTION_TIMEOUT"
def test_execution_timeout_rejects_more_than_seven_days():
client = SimStudioClient(api_key="test-api-key")
with pytest.raises(SimStudioError) as exc_info:
client.execute_workflow(
"workflow-id",
{},
async_execution=True,
execution_timeout_seconds=604_801,
)
assert exc_info.value.code == "INVALID_EXECUTION_TIMEOUT"
def test_execute_with_retry_forwards_execution_timeout():
client = SimStudioClient(api_key="test-api-key")
expected = Mock()
with patch.object(client, "execute_workflow", return_value=expected) as execute_workflow:
result = client.execute_with_retry(
"workflow-id",
{"message": "hello"},
async_execution=True,
execution_timeout_seconds=90,
)
assert result is expected
execute_workflow.assert_called_once_with(
"workflow-id",
{"message": "hello"},
timeout=30.0,
stream=None,
selected_outputs=None,
async_execution=True,
execution_timeout_seconds=90,
)
@patch('simstudio.requests.Session.get')
def test_get_job_status_success(mock_get):
"""Test getting job status."""
@@ -534,4 +607,4 @@ def test_execute_workflow_with_dict_input_spreads_at_root(mock_post):
assert request_body["ticker"] == "NVDA"
assert request_body["quantity"] == 100
assert "input" not in request_body # Should not wrap in input field
assert "input" not in request_body # Should not wrap in input field
@@ -39,6 +39,7 @@ export interface EnvFlagsMockState {
isSessionPoliciesEnabled: boolean
isForkingEnabled: boolean
isRemoteSandboxEnabled: boolean
isMothershipSandboxEnabled: boolean
isDocSandboxEnabled: boolean
isOllamaConfigured: boolean
isAzureConfigured: boolean
@@ -88,6 +89,7 @@ const defaultEnvFlagsState: EnvFlagsMockState = {
isDataDrainsEnabled: false,
isForkingEnabled: false,
isRemoteSandboxEnabled: false,
isMothershipSandboxEnabled: false,
isDocSandboxEnabled: false,
isOllamaConfigured: false,
isAzureConfigured: false,
@@ -5,8 +5,8 @@ import { vi } from 'vitest'
* `@/lib/logs/execution/logging-session`. Every instance method is backed by a
* shared `vi.fn()` so tests that construct multiple sessions observe identical
* mock state. `mockSafeStart` defaults to `true` because callers branch on the
* boolean result. Projection methods return their input; other methods resolve
* to `undefined`.
* boolean result. Display projection methods return their input, diagnostic projection
* fails closed to structural metadata, and other methods resolve to `undefined`.
*
* @example
* ```ts
@@ -26,9 +26,14 @@ export const loggingSessionMockFns = {
mockWaitForCompletion: vi.fn().mockResolvedValue(undefined),
mockWaitForPostExecution: vi.fn().mockResolvedValue(undefined),
mockSetTrustedExecutionCorrelation: vi.fn(),
mockSetExecutionDeadlineAt: vi.fn(),
mockProjectBlockLogsForDisplay: vi.fn(async (logs: unknown) => logs),
mockProjectDisplayContent: vi.fn(async (content: unknown) => content),
mockProjectLiveDisplayText: vi.fn(async (_field: string, value: string) => ({ value })),
mockProjectDiagnosticError: vi.fn((error: unknown, _details: Record<string, unknown> = {}) => ({
errorType: error instanceof Error ? 'error' : error === null ? 'null' : typeof error,
hasStack: error instanceof Error && typeof error.stack === 'string',
})),
mockSafeComplete: vi.fn().mockResolvedValue(undefined),
mockSafeCompleteWithError: vi.fn().mockResolvedValue(undefined),
mockSafeCompleteWithCancellation: vi.fn().mockResolvedValue(undefined),
@@ -53,9 +58,11 @@ function buildLoggingSessionInstance() {
waitForCompletion: loggingSessionMockFns.mockWaitForCompletion,
waitForPostExecution: loggingSessionMockFns.mockWaitForPostExecution,
setTrustedExecutionCorrelation: loggingSessionMockFns.mockSetTrustedExecutionCorrelation,
setExecutionDeadlineAt: loggingSessionMockFns.mockSetExecutionDeadlineAt,
projectBlockLogsForDisplay: loggingSessionMockFns.mockProjectBlockLogsForDisplay,
projectDisplayContent: loggingSessionMockFns.mockProjectDisplayContent,
projectLiveDisplayText: loggingSessionMockFns.mockProjectLiveDisplayText,
projectDiagnosticError: loggingSessionMockFns.mockProjectDiagnosticError,
safeComplete: loggingSessionMockFns.mockSafeComplete,
safeCompleteWithError: loggingSessionMockFns.mockSafeCompleteWithError,
safeCompleteWithCancellation: loggingSessionMockFns.mockSafeCompleteWithCancellation,
+42
View File
@@ -594,6 +594,16 @@ export const schemaMock = {
size: 'size',
deletedAt: 'deletedAt',
uploadedAt: 'uploadedAt',
updatedAt: 'updatedAt',
contentUpdatedAt: 'contentUpdatedAt',
secretProvenanceVersion: 'secretProvenanceVersion',
},
workspaceFileSecretProvenance: {
fileId: 'fileId',
contentUpdatedAt: 'contentUpdatedAt',
status: 'status',
entries: 'entries',
updatedAt: 'updatedAt',
},
permissionTypeEnum: 'permissionTypeEnum',
workspaceInvitationStatusEnum: 'workspaceInvitationStatusEnum',
@@ -625,10 +635,18 @@ export const schemaMock = {
workspaceId: 'workspaceId',
key: 'key',
data: 'data',
secretProvenanceVersion: 'secretProvenanceVersion',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
deletedAt: 'deletedAt',
},
memorySecretProvenance: {
memoryId: 'memoryId',
contentHash: 'contentHash',
status: 'status',
entries: 'entries',
updatedAt: 'updatedAt',
},
knowledgeBase: {
id: 'id',
userId: 'userId',
@@ -682,8 +700,16 @@ export const schemaMock = {
externalId: 'externalId',
contentHash: 'contentHash',
sourceUrl: 'sourceUrl',
secretProvenanceVersion: 'secretProvenanceVersion',
uploadedAt: 'uploadedAt',
},
documentSecretProvenance: {
documentId: 'documentId',
sourceHash: 'sourceHash',
status: 'status',
entries: 'entries',
updatedAt: 'updatedAt',
},
knowledgeBaseTagDefinitions: {
id: 'id',
knowledgeBaseId: 'knowledgeBaseId',
@@ -700,6 +726,7 @@ export const schemaMock = {
chunkIndex: 'chunkIndex',
chunkHash: 'chunkHash',
content: 'content',
secretProvenanceVersion: 'secretProvenanceVersion',
contentLength: 'contentLength',
tokenCount: 'tokenCount',
embedding: 'embedding',
@@ -728,6 +755,13 @@ export const schemaMock = {
createdAt: 'createdAt',
updatedAt: 'updatedAt',
},
embeddingSecretProvenance: {
embeddingId: 'embeddingId',
contentHash: 'contentHash',
status: 'status',
entries: 'entries',
updatedAt: 'updatedAt',
},
docsEmbeddings: {
chunkId: 'chunkId',
chunkText: 'chunkText',
@@ -1146,10 +1180,18 @@ export const schemaMock = {
workspaceId: 'workspaceId',
data: 'data',
position: 'position',
secretProvenanceVersion: 'secretProvenanceVersion',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
createdBy: 'createdBy',
},
userTableRowSecretProvenance: {
rowId: 'rowId',
contentUpdatedAt: 'contentUpdatedAt',
status: 'status',
entries: 'entries',
updatedAt: 'updatedAt',
},
tableJobs: {
id: 'id',
tableId: 'tableId',
@@ -19,6 +19,7 @@ export const storageServiceMockFns = {
mockDownloadFile: vi.fn(),
mockDeleteFile: vi.fn(),
mockHeadObject: vi.fn(),
mockVerifyPresignedUploadReceipt: vi.fn(),
mockGeneratePresignedUploadUrl: vi.fn(),
mockGenerateBatchPresignedUploadUrls: vi.fn(),
mockGeneratePresignedDownloadUrl: vi.fn(),
@@ -39,6 +40,7 @@ export const storageServiceMock = {
downloadFile: storageServiceMockFns.mockDownloadFile,
deleteFile: storageServiceMockFns.mockDeleteFile,
headObject: storageServiceMockFns.mockHeadObject,
verifyPresignedUploadReceipt: storageServiceMockFns.mockVerifyPresignedUploadReceipt,
generatePresignedUploadUrl: storageServiceMockFns.mockGeneratePresignedUploadUrl,
generateBatchPresignedUploadUrls: storageServiceMockFns.mockGenerateBatchPresignedUploadUrls,
generatePresignedDownloadUrl: storageServiceMockFns.mockGeneratePresignedDownloadUrl,
+6 -2
View File
@@ -62,7 +62,9 @@ const result = await client.executeWorkflow('workflow-id', 'NVDA');
// With options
const result = await client.executeWorkflow('workflow-id', { message: 'Hello' }, {
timeout: 60000
timeout: 60000,
async: true,
executionTimeoutSeconds: 3600
});
```
@@ -74,6 +76,7 @@ const result = await client.executeWorkflow('workflow-id', { message: 'Hello' },
- `stream` (boolean): Enable streaming responses
- `selectedOutputs` (string[]): Block outputs to stream (e.g., `["agent1.content"]`)
- `async` (boolean): Execute asynchronously and return execution ID
- `executionTimeoutSeconds` (number): Server-side async execution cap from 1 to 604800 seconds. Requires `async: true` and cannot extend the account policy.
**Returns:** `Promise<WorkflowExecutionResult | AsyncExecutionResult>`
@@ -323,6 +326,7 @@ interface ExecutionOptions {
stream?: boolean;
selectedOutputs?: string[];
async?: boolean;
executionTimeoutSeconds?: number;
}
```
@@ -533,4 +537,4 @@ bun run dev
## License
Apache-2.0
Apache-2.0
+37
View File
@@ -174,6 +174,43 @@ describe('SimStudioClient', () => {
const calls = vi.mocked(mockFetch).mock.calls
expect(calls[0][1]?.headers).not.toHaveProperty('X-Execution-Mode')
})
it('sets the server-side timeout header for async execution', async () => {
vi.mocked(mockFetch).mockResolvedValue({
ok: true,
status: 202,
json: vi.fn().mockResolvedValue({ success: true, jobId: 'job-1', async: true }),
headers: { get: vi.fn().mockReturnValue(null) },
} as any)
await client.executeWorkflow('workflow-id', {}, { async: true, executionTimeoutSeconds: 90 })
expect(vi.mocked(mockFetch).mock.calls[0][1]?.headers).toMatchObject({
'X-Execution-Mode': 'async',
'X-Execution-Timeout-Seconds': '90',
})
})
it('rejects a server-side timeout for sync execution', async () => {
await expect(
client.executeWorkflow('workflow-id', {}, { executionTimeoutSeconds: 90 })
).rejects.toMatchObject({ code: 'INVALID_EXECUTION_TIMEOUT' })
expect(mockFetch).not.toHaveBeenCalled()
})
it('rejects a server-side timeout above seven days', async () => {
await expect(
client.executeWorkflow(
'workflow-id',
{},
{
async: true,
executionTimeoutSeconds: 604_801,
}
)
).rejects.toMatchObject({ code: 'INVALID_EXECUTION_TIMEOUT' })
expect(mockFetch).not.toHaveBeenCalled()
})
})
describe('getJobStatus', () => {
+36 -2
View File
@@ -37,12 +37,24 @@ export interface WorkflowStatus {
}
export interface ExecutionOptions {
/** Client-side HTTP timeout in milliseconds. */
timeout?: number
stream?: boolean
selectedOutputs?: string[]
async?: boolean
/** Server-side async execution cap in seconds (1–604800). */
executionTimeoutSeconds?: number
}
export type SyncExecutionOptions = Omit<ExecutionOptions, 'async' | 'executionTimeoutSeconds'> & {
/** Async mode is controlled by executeWorkflowSync and cannot be overridden. */
async?: never
/** Server-side execution timeout overrides are async-only. */
executionTimeoutSeconds?: never
}
const MAX_EXECUTION_TIMEOUT_SECONDS = 604_800
export interface AsyncExecutionResult {
success: boolean
jobId: string
@@ -216,7 +228,26 @@ export class SimStudioClient {
options: ExecutionOptions = {}
): Promise<WorkflowExecutionResult | AsyncExecutionResult> {
const url = `${this.baseUrl}/api/workflows/${workflowId}/execute`
const { timeout = 30000, stream, selectedOutputs, async } = options
const { timeout = 30000, stream, selectedOutputs, async, executionTimeoutSeconds } = options
if (executionTimeoutSeconds !== undefined) {
if (!async) {
throw new SimStudioError(
'executionTimeoutSeconds is supported only for async executions',
'INVALID_EXECUTION_TIMEOUT'
)
}
if (
!Number.isSafeInteger(executionTimeoutSeconds) ||
executionTimeoutSeconds < 1 ||
executionTimeoutSeconds > MAX_EXECUTION_TIMEOUT_SECONDS
) {
throw new SimStudioError(
`executionTimeoutSeconds must be an integer between 1 and ${MAX_EXECUTION_TIMEOUT_SECONDS}`,
'INVALID_EXECUTION_TIMEOUT'
)
}
}
try {
const timeoutPromise = new Promise<never>((_, reject) => {
@@ -230,6 +261,9 @@ export class SimStudioClient {
if (async) {
headers['X-Execution-Mode'] = 'async'
}
if (executionTimeoutSeconds !== undefined) {
headers['X-Execution-Timeout-Seconds'] = executionTimeoutSeconds.toString()
}
let jsonBody: any = {}
if (input !== undefined && input !== null) {
@@ -341,7 +375,7 @@ export class SimStudioClient {
async executeWorkflowSync(
workflowId: string,
input?: any,
options: ExecutionOptions = {}
options: SyncExecutionOptions = {}
): Promise<WorkflowExecutionResult> {
const syncOptions = { ...options, async: false }
return this.executeWorkflow(workflowId, input, syncOptions) as Promise<WorkflowExecutionResult>
+4
View File
@@ -53,6 +53,10 @@
"./sso-domain": {
"types": "./src/sso-domain.ts",
"default": "./src/sso-domain.ts"
},
"./sandbox-references": {
"types": "./src/sandbox-references.ts",
"default": "./src/sandbox-references.ts"
}
},
"scripts": {
@@ -0,0 +1,82 @@
import {
immutableE2BTemplateRef,
isImmutableDaytonaSnapshotRef,
isImmutableE2BTemplateRef,
isValidE2BTemplateName,
isValidE2BTemplateReferenceName,
isValidSandboxReleaseGeneration,
normalizeSandboxProvider,
} from '@sim/utils/sandbox-references'
import { describe, expect, it } from 'vitest'
const BUILD_ID = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
describe('immutable sandbox references', () => {
it('normalizes only registered sandbox providers', () => {
expect(normalizeSandboxProvider('E2B')).toBe('e2b')
expect(normalizeSandboxProvider('Daytona')).toBe('daytona')
expect(normalizeSandboxProvider('modal')).toBeUndefined()
expect(normalizeSandboxProvider(undefined)).toBeUndefined()
})
it.each([
`sim-function:${BUILD_ID}`,
`team_sim-function:${BUILD_ID}`,
`acme/sim-function:${BUILD_ID}`,
])('accepts an exact E2B build reference: %s', (value) => {
expect(isImmutableE2BTemplateRef(value)).toBe(true)
})
it.each([
'sim-function',
'sim-function:default',
'sim-function:latest',
'sim-function:v1',
`sim-function:stable:${BUILD_ID}`,
`team/child/sim-function:${BUILD_ID}`,
`Sim-function:${BUILD_ID}`,
`sim.function:${BUILD_ID}`,
BUILD_ID,
])('rejects a movable or incomplete E2B reference: %s', (value) => {
expect(isImmutableE2BTemplateRef(value)).toBe(false)
})
it('creates an exact E2B build reference', () => {
expect(immutableE2BTemplateRef('sim-function', BUILD_ID)).toBe(`sim-function:${BUILD_ID}`)
})
it.each(['sim-function', 'team_sim-function'])(
'accepts an untagged E2B template family: %s',
(value) => {
expect(isValidE2BTemplateName(value)).toBe(true)
}
)
it.each(['sim-function:latest', 'team/sim-function', 'Sim-function', 'sim.function', ''])(
'rejects an invalid E2B template family: %s',
(value) => {
expect(isValidE2BTemplateName(value)).toBe(false)
}
)
it('accepts one namespace only for an exact reference name', () => {
expect(isValidE2BTemplateReferenceName('acme/sim-function')).toBe(true)
expect(isValidE2BTemplateName('acme/sim-function')).toBe(false)
expect(isValidE2BTemplateReferenceName('acme/team/sim-function')).toBe(false)
})
it('requires a Daytona snapshot ID rather than a name', () => {
expect(isImmutableDaytonaSnapshotRef(BUILD_ID)).toBe(true)
expect(isImmutableDaytonaSnapshotRef('sim-function:2026-08-03')).toBe(false)
expect(isImmutableDaytonaSnapshotRef('sim-function')).toBe(false)
})
it('accepts only positive safe release generations', () => {
expect(isValidSandboxReleaseGeneration('1785792000000')).toBe(true)
expect(isValidSandboxReleaseGeneration(1785792000000)).toBe(true)
expect(isValidSandboxReleaseGeneration('0')).toBe(false)
expect(isValidSandboxReleaseGeneration('1.5')).toBe(false)
expect(isValidSandboxReleaseGeneration('01')).toBe(false)
expect(isValidSandboxReleaseGeneration(Number.MAX_SAFE_INTEGER)).toBe(false)
})
})
+76
View File
@@ -0,0 +1,76 @@
import { isValidUuid } from '@sim/utils/id'
export const IMMUTABLE_E2B_TEMPLATE_REF_ERROR =
'must be an immutable E2B build reference in the form <template>:<build-id>'
export const IMMUTABLE_DAYTONA_SNAPSHOT_REF_ERROR =
'must be an immutable Daytona snapshot ID, not a snapshot name'
export const SANDBOX_RELEASE_GENERATION_ERROR = 'must be a positive safe integer release generation'
export const SANDBOX_PROVIDER_IDS = ['e2b', 'daytona'] as const
export type SandboxProviderName = (typeof SANDBOX_PROVIDER_IDS)[number]
/** Leaves three decimal digits for a materializer revision in one safe integer. */
export const MAX_SANDBOX_RELEASE_GENERATION = Math.floor((Number.MAX_SAFE_INTEGER - 999) / 1000)
const E2B_TEMPLATE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,62}$/
const E2B_TEMPLATE_REFERENCE_NAME_PATTERN =
/^(?:[a-z0-9][a-z0-9_-]{0,62}\/)?[a-z0-9][a-z0-9_-]{0,62}$/
/** Normalizes a configured provider and rejects values outside the supported registry. */
export function normalizeSandboxProvider(
value: string | undefined
): SandboxProviderName | undefined {
if (!value) return undefined
const normalized = value.toLowerCase()
return SANDBOX_PROVIDER_IDS.find((provider) => provider === normalized)
}
/** E2B template families use the provider's untagged, lowercase name grammar. */
export function isValidE2BTemplateName(value: string): boolean {
return E2B_TEMPLATE_NAME_PATTERN.test(value)
}
/** Exact E2B refs may include one documented lowercase namespace segment. */
export function isValidE2BTemplateReferenceName(value: string): boolean {
return E2B_TEMPLATE_REFERENCE_NAME_PATTERN.test(value)
}
/** Validates the monotonic release identity operators deploy with a Function base. */
export function isValidSandboxReleaseGeneration(value: string | number): boolean {
const generation = typeof value === 'number' ? value : Number(value)
return (
Number.isSafeInteger(generation) &&
generation > 0 &&
generation <= MAX_SANDBOX_RELEASE_GENERATION &&
String(generation) === String(value)
)
}
/**
* E2B resolves `<template>:<build-id>` directly to one immutable template build.
* Human tags are deliberately rejected because E2B permits reassigning them.
*/
export function isImmutableE2BTemplateRef(value: string): boolean {
const separator = value.lastIndexOf(':')
if (separator <= 0 || separator === value.length - 1) return false
const template = value.slice(0, separator)
const buildId = value.slice(separator + 1)
return isValidE2BTemplateReferenceName(template) && isValidUuid(buildId)
}
/** Daytona accepts a snapshot ID anywhere it accepts a name; only the ID is immutable. */
export function isImmutableDaytonaSnapshotRef(value: string): boolean {
return isValidUuid(value)
}
/** Builds the exact E2B reference operators should deploy after a successful build. */
export function immutableE2BTemplateRef(templateName: string, buildId: string): string {
const ref = `${templateName}:${buildId}`
if (!isImmutableE2BTemplateRef(ref)) {
throw new Error(`E2B template reference ${IMMUTABLE_E2B_TEMPLATE_REF_ERROR}`)
}
return ref
}