Skip to content

Commit 53886b1

Browse files
committed
refactor(mothership): last sweep-debt — session-key constructor, shared leaf walker
chatSandboxSessionKey is the one constructor for the per-chat sandbox identity (the key doubles as the E2B lease and the workbench file-bridge scope; two hand-built literals drifting apart would split a chat across machines). collectStringLeaves moves into the executor's reference-validation module — lint and the deps augmentation each carried an identical copy of the walk. Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent c999ef2 commit 53886b1

6 files changed

Lines changed: 32 additions & 19 deletions

File tree

apps/sim/executor/utils/reference-validation.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,14 @@ export function createCombinedPattern(): RegExp {
144144
'g'
145145
)
146146
}
147+
148+
/**
149+
* Collects every string leaf in a nested value — the shared walk for reference/env-token
150+
* audits over block inputs (lint, deps, and the agent-cli mirrors each carried a copy).
151+
*/
152+
export function collectStringLeaves(value: unknown, out: string[]): void {
153+
if (typeof value === 'string') out.push(value)
154+
else if (Array.isArray(value)) for (const item of value) collectStringLeaves(item, out)
155+
else if (typeof value === 'object' && value !== null)
156+
for (const item of Object.values(value)) collectStringLeaves(item, out)
157+
}

apps/sim/lib/mothership/tools/handlers/agent-cli/commands/deps.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {
55
agentCliOk,
66
} from '@/lib/mothership/tools/handlers/agent-cli/types'
77
import { normalizeName, SPECIAL_REFERENCE_PREFIXES } from '@/executor/constants'
8-
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
8+
import {
9+
collectStringLeaves,
10+
createEnvVarPattern,
11+
createReferencePattern,
12+
} from '@/executor/utils/reference-validation'
913

1014
/**
1115
* `workflow deps <workflowId> <blockId>` — everything one block consumes, so the
@@ -29,13 +33,6 @@ interface DepView {
2933
paths?: string[]
3034
}
3135

32-
function stringLeaves(value: unknown, out: string[]): void {
33-
if (typeof value === 'string') out.push(value)
34-
else if (Array.isArray(value)) for (const item of value) stringLeaves(item, out)
35-
else if (typeof value === 'object' && value !== null)
36-
for (const item of Object.values(value)) stringLeaves(item, out)
37-
}
38-
3936
export const workflowDepsCommand: AgentCliCommand = {
4037
path: ['workflow', 'deps'],
4138
summary:
@@ -61,7 +58,7 @@ export const workflowDepsCommand: AgentCliCommand = {
6158
}
6259

6360
const leaves: string[] = []
64-
stringLeaves(block.subBlocks ?? block, leaves)
61+
collectStringLeaves(block.subBlocks ?? block, leaves)
6562

6663
const byToken = new Map<string, DepView>()
6764
const envs = new Set<string>()

apps/sim/lib/mothership/tools/handlers/function-execute.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import type {
2525
ToolExecutionContext,
2626
ToolExecutionResult,
2727
} from '@/lib/mothership/tool-executor/types'
28+
import { chatSandboxSessionKey } from '@/lib/mothership/tools/sandbox-session'
2829
import {
2930
CopilotCodeSecretAccessError,
3031
type MaterializedCopilotCodeSecrets,
@@ -515,7 +516,7 @@ export async function executeFunctionExecute(
515516
// bootstrapped into it. Chat-less executions (one-shot, headless) stay
516517
// ephemeral.
517518
if (context.chatId) {
518-
enrichedParams.sandboxSessionKey = `mothership-chat:${context.chatId}`
519+
enrichedParams.sandboxSessionKey = chatSandboxSessionKey(context.chatId)
519520
}
520521
// The copilot tool doc promises `timeout` in SECONDS ("Sim converts to
521522
// milliseconds", default 10, cap 300); the underlying function tool takes

apps/sim/lib/mothership/tools/handlers/sim-cli.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
matchAgentCliCommand,
1818
} from '@/lib/mothership/tools/handlers/agent-cli'
1919
import { applyPipeline, splitPipeline } from '@/lib/mothership/tools/handlers/sim-cli-pipe'
20+
import { chatSandboxSessionKey } from '@/lib/mothership/tools/sandbox-session'
2021

2122
const logger = createLogger('MothershipSimCli')
2223

@@ -61,7 +62,7 @@ export async function executeSimCli(
6162
// `--text @channel` stays literal), and the server's filesystem is never
6263
// readable from model argv. A token that names no sandbox file is simply
6364
// absent from the map; the resolver's refusal then says so.
64-
const sessionKey = context.chatId ? `mothership-chat:${context.chatId}` : null
65+
const sessionKey = context.chatId ? chatSandboxSessionKey(context.chatId) : null
6566
const fileArguments: Record<string, string> = {}
6667
if (sessionKey) {
6768
for (const token of args) {

apps/sim/lib/mothership/tools/sandbox-session.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import { mintDelegationToken } from '@/lib/mothership/chat/delegation'
77

88
const logger = createLogger('MothershipSandboxSession')
99

10+
/**
11+
* The per-chat sandbox identity. One constructor: the key doubles as the E2B lease key
12+
* AND the workbench file-bridge scope, so two hand-built copies drifting apart would
13+
* silently split a chat across two machines.
14+
*/
15+
export function chatSandboxSessionKey(chatId: string): string {
16+
return `mothership-chat:${chatId}`
17+
}
18+
1019
/**
1120
* Installed once per fresh session sandbox until the mothership images bake the
1221
* CLI in. `command -v` keeps the install one-time: a sandbox that already has

apps/sim/lib/workflows/editing/lint.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getBlock } from '@/blocks'
22
import { isTriggerBlockType, normalizeName, SPECIAL_REFERENCE_PREFIXES } from '@/executor/constants'
3+
import { collectStringLeaves } from '@/executor/utils/reference-validation'
34
import {
45
collectBlockFieldIssues,
56
extractBlockParams,
@@ -388,13 +389,6 @@ export function formatWorkflowLintMessage(lint: WorkflowLintIssueView) {
388389
const BLOCK_REF_TOKEN = /<([^<>]+)>/g
389390
const REF_TOKEN_SHAPE = /^[A-Za-z_][\w-]*(?:[\w\s-]*[\w-])?\.[A-Za-z0-9_.[\]]+$/
390391

391-
function stringLeavesForLint(value: unknown, out: string[]): void {
392-
if (typeof value === 'string') out.push(value)
393-
else if (Array.isArray(value)) for (const item of value) stringLeavesForLint(item, out)
394-
else if (typeof value === 'object' && value !== null)
395-
for (const item of Object.values(value)) stringLeavesForLint(item, out)
396-
}
397-
398392
export function collectDanglingBlockOutputReferences(
399393
workflowState: Pick<WorkflowState, 'blocks'>
400394
): WorkflowLintUnresolvedReference[] {
@@ -409,7 +403,7 @@ export function collectDanglingBlockOutputReferences(
409403
for (const [subBlockId, subBlock] of Object.entries(block.subBlocks ?? {})) {
410404
if (subBlockId === 'code') continue
411405
const leaves: string[] = []
412-
stringLeavesForLint((subBlock as { value?: unknown })?.value, leaves)
406+
collectStringLeaves((subBlock as { value?: unknown })?.value, leaves)
413407
const dangling = new Set<string>()
414408
for (const leaf of leaves) {
415409
for (const match of leaf.matchAll(BLOCK_REF_TOKEN)) {

0 commit comments

Comments
 (0)