@tangle-network/agent-app 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ // src/harness/index.ts
2
+ var KNOWN_HARNESSES = [
3
+ "opencode",
4
+ "claude-code",
5
+ "kimi-code",
6
+ "codex",
7
+ "amp",
8
+ "factory-droids",
9
+ "pi",
10
+ "hermes",
11
+ "forge",
12
+ "openclaw",
13
+ "acp",
14
+ "cursor",
15
+ "cli-base"
16
+ ];
17
+ var DEFAULT_HARNESS = "opencode";
18
+ var HARNESS_SET = new Set(KNOWN_HARNESSES);
19
+ function isHarness(value) {
20
+ return typeof value === "string" && HARNESS_SET.has(value);
21
+ }
22
+ function coerceHarness(value, fallback = DEFAULT_HARNESS) {
23
+ return isHarness(value) ? value : fallback;
24
+ }
25
+ function resolveSessionHarness(input = {}) {
26
+ const fallback = input.fallback ?? DEFAULT_HARNESS;
27
+ if (isHarness(input.sessionHarness)) {
28
+ const locked = input.sessionHarness;
29
+ const swapAttempted = isHarness(input.requested) && input.requested !== locked;
30
+ return { harness: locked, locked: true, swapAttempted };
31
+ }
32
+ const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback));
33
+ return { harness, locked: false, swapAttempted: false };
34
+ }
35
+ var HARNESS_MODEL_POLICIES = {
36
+ "claude-code": {
37
+ providers: ["anthropic"],
38
+ preferred: [/^anthropic\/claude-opus-[\d.-]+$/, /^anthropic\/claude-sonnet-[\d.-]+$/, /^anthropic\//]
39
+ },
40
+ codex: {
41
+ providers: ["openai"],
42
+ preferred: [/^openai\/gpt-\d+(\.\d+)?$/, /^openai\/gpt/, /^openai\//]
43
+ },
44
+ "kimi-code": { providers: ["moonshot"], preferred: [/^moonshot\//] }
45
+ };
46
+ var PROVIDER_PREFERRED_HARNESS = {
47
+ anthropic: "claude-code",
48
+ openai: "codex",
49
+ moonshot: "kimi-code"
50
+ };
51
+ function modelProvider(modelId) {
52
+ const slash = modelId.indexOf("/");
53
+ return slash > 0 ? modelId.slice(0, slash) : null;
54
+ }
55
+ function isModelCompatibleWithHarness(harness, modelId) {
56
+ const policy = HARNESS_MODEL_POLICIES[harness];
57
+ if (!policy || policy.providers === null) return true;
58
+ const provider = modelProvider(modelId);
59
+ if (!provider) return true;
60
+ return policy.providers.includes(provider);
61
+ }
62
+ var numericDesc = new Intl.Collator(void 0, { numeric: true, sensitivity: "base" });
63
+ function snapModelToHarness(harness, modelId, canonicalIds) {
64
+ if (isModelCompatibleWithHarness(harness, modelId)) return modelId;
65
+ const policy = HARNESS_MODEL_POLICIES[harness];
66
+ if (!policy) return modelId;
67
+ for (const pattern of policy.preferred) {
68
+ const matches = canonicalIds.filter((id) => pattern.test(id)).sort((a, b) => numericDesc.compare(b, a));
69
+ if (matches.length > 0) return matches[0];
70
+ }
71
+ return canonicalIds.find((id) => isModelCompatibleWithHarness(harness, id)) ?? modelId;
72
+ }
73
+ function snapHarnessToModel(harness, modelId) {
74
+ if (isModelCompatibleWithHarness(harness, modelId)) return harness;
75
+ const provider = modelProvider(modelId);
76
+ return provider && PROVIDER_PREFERRED_HARNESS[provider] || "opencode";
77
+ }
78
+ function assertHarnessModelCompatible(harness, modelId) {
79
+ if (!isModelCompatibleWithHarness(harness, modelId)) {
80
+ const provider = modelProvider(modelId);
81
+ throw new Error(
82
+ `Harness "${harness}" cannot run model "${modelId}" (provider "${provider}"). Use ${PROVIDER_PREFERRED_HARNESS[provider ?? ""] ?? "a router-backed harness (opencode)"} or an allowed model.`
83
+ );
84
+ }
85
+ }
86
+
87
+ export {
88
+ KNOWN_HARNESSES,
89
+ DEFAULT_HARNESS,
90
+ isHarness,
91
+ coerceHarness,
92
+ resolveSessionHarness,
93
+ HARNESS_MODEL_POLICIES,
94
+ PROVIDER_PREFERRED_HARNESS,
95
+ modelProvider,
96
+ isModelCompatibleWithHarness,
97
+ snapModelToHarness,
98
+ snapHarnessToModel,
99
+ assertHarnessModelCompatible
100
+ };
101
+ //# sourceMappingURL=chunk-5VXPDXZJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/harness/index.ts"],"sourcesContent":["/**\n * Coding-agent harness selection — taxonomy, coercion, and the session-lock invariant.\n *\n * A \"harness\" is the coding-agent CLI a sandbox drives (opencode / codex /\n * claude-code / …). The shell governs WHICH harness a chat session uses and\n * enforces that a session is LOCKED to the harness it started with — the model\n * may change mid-session, the harness may not (swapping it mid-session would\n * orphan the session's running agent state). Every product otherwise hand-rolls\n * this and hard-codes a single harness; this is the one place the rule lives.\n *\n * Substrate-free: the harness list mirrors the sandbox SDK's `BackendType` as a\n * plain string union (no sandbox dependency). The consumer owns storage — which\n * harness a workspace defaults to, which one a session locked — and maps the\n * resolved value onto the SDK's `backend.type`.\n */\n\n/** The known coding-agent backends. Mirrors `@tangle-network/sandbox`'s\n * `BackendType`; kept structural so this module needs no sandbox dependency. */\nexport const KNOWN_HARNESSES = [\n 'opencode',\n 'claude-code',\n 'kimi-code',\n 'codex',\n 'amp',\n 'factory-droids',\n 'pi',\n 'hermes',\n 'forge',\n 'openclaw',\n 'acp',\n 'cursor',\n 'cli-base',\n] as const\n\nexport type Harness = (typeof KNOWN_HARNESSES)[number]\n\nexport const DEFAULT_HARNESS: Harness = 'opencode'\n\nconst HARNESS_SET: ReadonlySet<string> = new Set(KNOWN_HARNESSES)\n\nexport function isHarness(value: unknown): value is Harness {\n return typeof value === 'string' && HARNESS_SET.has(value)\n}\n\n/** Coerce an arbitrary value to a known harness, falling back (default `opencode`). */\nexport function coerceHarness(value: unknown, fallback: Harness = DEFAULT_HARNESS): Harness {\n return isHarness(value) ? value : fallback\n}\n\nexport interface ResolveSessionHarnessInput {\n /** The harness already locked to this session (recorded at its first turn). */\n sessionHarness?: unknown\n /** The harness requested now — a new session's choice, or a turn's attempt to switch. */\n requested?: unknown\n /** The workspace's default harness, used only when starting a fresh session. */\n workspaceDefault?: unknown\n /** Final fallback when nothing else resolves (default `opencode`). */\n fallback?: Harness\n}\n\nexport interface ResolvedSessionHarness {\n /** The harness to actually run — the locked one when the session already has it. */\n harness: Harness\n /** True when the session already had a locked harness (this turn did not pick it). */\n locked: boolean\n /** True when `requested` differs from the locked harness — a forbidden mid-session\n * swap the caller should reject or warn on. The lock always wins regardless. */\n swapAttempted: boolean\n}\n\n/**\n * Resolve the harness for a turn, enforcing the session lock.\n *\n * - **Session already started** (`sessionHarness` is a known harness): that harness\n * wins (`locked: true`); a differing `requested` sets `swapAttempted` so the caller\n * can reject the swap. The model is a separate per-turn concern and is unaffected.\n * - **Fresh session**: pick `requested → workspaceDefault → fallback`. The caller\n * persists the result as the session's lock for every subsequent turn.\n */\nexport function resolveSessionHarness(input: ResolveSessionHarnessInput = {}): ResolvedSessionHarness {\n const fallback = input.fallback ?? DEFAULT_HARNESS\n if (isHarness(input.sessionHarness)) {\n const locked = input.sessionHarness\n const swapAttempted = isHarness(input.requested) && input.requested !== locked\n return { harness: locked, locked: true, swapAttempted }\n }\n const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback))\n return { harness, locked: false, swapAttempted: false }\n}\n\n/**\n * Harness ↔ model compatibility policy — the ONE source of truth, server-side.\n *\n * Native CLI harnesses are vendor-locked: claude-code only drives Anthropic\n * models, codex only OpenAI, kimi-code only Moonshot. Router-backed harnesses\n * (opencode, etc.) accept any catalog model (`providers: null`). The pickers in\n * sandbox-ui keep the pair coherent in the UI; this is the same policy the SHELL\n * enforces so a bypassed/forged request can't pair claude-code with a gpt model\n * and fail at the sidecar. Operates on plain canonical ids (\"provider/model\") so\n * it stays substrate-free — no model-catalog or UI dependency.\n */\nexport interface HarnessModelPolicy {\n /** Canonical-id provider prefixes the harness can run; null = any. */\n providers: readonly string[] | null\n /** Snap-target patterns, best first; highest version within a pattern wins. */\n preferred: readonly RegExp[]\n}\n\nexport const HARNESS_MODEL_POLICIES: Partial<Record<Harness, HarnessModelPolicy>> = {\n 'claude-code': {\n providers: ['anthropic'],\n preferred: [/^anthropic\\/claude-opus-[\\d.-]+$/, /^anthropic\\/claude-sonnet-[\\d.-]+$/, /^anthropic\\//],\n },\n codex: {\n providers: ['openai'],\n preferred: [/^openai\\/gpt-\\d+(\\.\\d+)?$/, /^openai\\/gpt/, /^openai\\//],\n },\n 'kimi-code': { providers: ['moonshot'], preferred: [/^moonshot\\//] },\n}\n\n/** Native harness for a model's provider (anthropic → claude-code, …). */\nexport const PROVIDER_PREFERRED_HARNESS: Record<string, Harness> = {\n anthropic: 'claude-code',\n openai: 'codex',\n moonshot: 'kimi-code',\n}\n\n/** Provider prefix of a canonical id (\"anthropic/claude-…\" → \"anthropic\"). */\nexport function modelProvider(modelId: string): string | null {\n const slash = modelId.indexOf('/')\n return slash > 0 ? modelId.slice(0, slash) : null\n}\n\n/** Provider-less ids (sentinels like \"default\", or a session's own config) are\n * compatible everywhere — every harness honors its own configuration. */\nexport function isModelCompatibleWithHarness(harness: Harness, modelId: string): boolean {\n const policy = HARNESS_MODEL_POLICIES[harness]\n if (!policy || policy.providers === null) return true\n const provider = modelProvider(modelId)\n if (!provider) return true\n return policy.providers.includes(provider)\n}\n\nconst numericDesc = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })\n\n/** Keep `modelId` when the harness can run it; else the harness's best compatible\n * catalog id (preferred patterns in order, highest version). When nothing in the\n * catalog fits, return the original so the caller sees the incompatibility. */\nexport function snapModelToHarness(harness: Harness, modelId: string, canonicalIds: readonly string[]): string {\n if (isModelCompatibleWithHarness(harness, modelId)) return modelId\n const policy = HARNESS_MODEL_POLICIES[harness]\n if (!policy) return modelId\n for (const pattern of policy.preferred) {\n const matches = canonicalIds.filter((id) => pattern.test(id)).sort((a, b) => numericDesc.compare(b, a))\n if (matches.length > 0) return matches[0]!\n }\n return canonicalIds.find((id) => isModelCompatibleWithHarness(harness, id)) ?? modelId\n}\n\n/** Keep the harness when it can run `modelId`; else the model's native harness\n * (anthropic → claude-code, openai → codex), falling back to opencode. */\nexport function snapHarnessToModel(harness: Harness, modelId: string): Harness {\n if (isModelCompatibleWithHarness(harness, modelId)) return harness\n const provider = modelProvider(modelId)\n return (provider && PROVIDER_PREFERRED_HARNESS[provider]) || 'opencode'\n}\n\n/** Fail-loud server guard: throw when a harness is asked to run a model it can't.\n * Call before dispatching a sandbox turn so a bypassed UI can't reach the sidecar\n * with an incompatible pair. */\nexport function assertHarnessModelCompatible(harness: Harness, modelId: string): void {\n if (!isModelCompatibleWithHarness(harness, modelId)) {\n const provider = modelProvider(modelId)\n throw new Error(\n `Harness \"${harness}\" cannot run model \"${modelId}\" (provider \"${provider}\"). ` +\n `Use ${PROVIDER_PREFERRED_HARNESS[provider ?? ''] ?? 'a router-backed harness (opencode)'} or an allowed model.`,\n )\n }\n}\n"],"mappings":"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","names":[]}
@@ -50,5 +50,41 @@ interface ResolvedSessionHarness {
50
50
  * persists the result as the session's lock for every subsequent turn.
51
51
  */
52
52
  declare function resolveSessionHarness(input?: ResolveSessionHarnessInput): ResolvedSessionHarness;
53
+ /**
54
+ * Harness ↔ model compatibility policy — the ONE source of truth, server-side.
55
+ *
56
+ * Native CLI harnesses are vendor-locked: claude-code only drives Anthropic
57
+ * models, codex only OpenAI, kimi-code only Moonshot. Router-backed harnesses
58
+ * (opencode, etc.) accept any catalog model (`providers: null`). The pickers in
59
+ * sandbox-ui keep the pair coherent in the UI; this is the same policy the SHELL
60
+ * enforces so a bypassed/forged request can't pair claude-code with a gpt model
61
+ * and fail at the sidecar. Operates on plain canonical ids ("provider/model") so
62
+ * it stays substrate-free — no model-catalog or UI dependency.
63
+ */
64
+ interface HarnessModelPolicy {
65
+ /** Canonical-id provider prefixes the harness can run; null = any. */
66
+ providers: readonly string[] | null;
67
+ /** Snap-target patterns, best first; highest version within a pattern wins. */
68
+ preferred: readonly RegExp[];
69
+ }
70
+ declare const HARNESS_MODEL_POLICIES: Partial<Record<Harness, HarnessModelPolicy>>;
71
+ /** Native harness for a model's provider (anthropic → claude-code, …). */
72
+ declare const PROVIDER_PREFERRED_HARNESS: Record<string, Harness>;
73
+ /** Provider prefix of a canonical id ("anthropic/claude-…" → "anthropic"). */
74
+ declare function modelProvider(modelId: string): string | null;
75
+ /** Provider-less ids (sentinels like "default", or a session's own config) are
76
+ * compatible everywhere — every harness honors its own configuration. */
77
+ declare function isModelCompatibleWithHarness(harness: Harness, modelId: string): boolean;
78
+ /** Keep `modelId` when the harness can run it; else the harness's best compatible
79
+ * catalog id (preferred patterns in order, highest version). When nothing in the
80
+ * catalog fits, return the original so the caller sees the incompatibility. */
81
+ declare function snapModelToHarness(harness: Harness, modelId: string, canonicalIds: readonly string[]): string;
82
+ /** Keep the harness when it can run `modelId`; else the model's native harness
83
+ * (anthropic → claude-code, openai → codex), falling back to opencode. */
84
+ declare function snapHarnessToModel(harness: Harness, modelId: string): Harness;
85
+ /** Fail-loud server guard: throw when a harness is asked to run a model it can't.
86
+ * Call before dispatching a sandbox turn so a bypassed UI can't reach the sidecar
87
+ * with an incompatible pair. */
88
+ declare function assertHarnessModelCompatible(harness: Harness, modelId: string): void;
53
89
 
54
- export { DEFAULT_HARNESS, type Harness, KNOWN_HARNESSES, type ResolveSessionHarnessInput, type ResolvedSessionHarness, coerceHarness, isHarness, resolveSessionHarness };
90
+ export { DEFAULT_HARNESS, HARNESS_MODEL_POLICIES, type Harness, type HarnessModelPolicy, KNOWN_HARNESSES, PROVIDER_PREFERRED_HARNESS, type ResolveSessionHarnessInput, type ResolvedSessionHarness, assertHarnessModelCompatible, coerceHarness, isHarness, isModelCompatibleWithHarness, modelProvider, resolveSessionHarness, snapHarnessToModel, snapModelToHarness };
@@ -1,15 +1,29 @@
1
1
  import {
2
2
  DEFAULT_HARNESS,
3
+ HARNESS_MODEL_POLICIES,
3
4
  KNOWN_HARNESSES,
5
+ PROVIDER_PREFERRED_HARNESS,
6
+ assertHarnessModelCompatible,
4
7
  coerceHarness,
5
8
  isHarness,
6
- resolveSessionHarness
7
- } from "../chunk-SD2H4FWY.js";
9
+ isModelCompatibleWithHarness,
10
+ modelProvider,
11
+ resolveSessionHarness,
12
+ snapHarnessToModel,
13
+ snapModelToHarness
14
+ } from "../chunk-5VXPDXZJ.js";
8
15
  export {
9
16
  DEFAULT_HARNESS,
17
+ HARNESS_MODEL_POLICIES,
10
18
  KNOWN_HARNESSES,
19
+ PROVIDER_PREFERRED_HARNESS,
20
+ assertHarnessModelCompatible,
11
21
  coerceHarness,
12
22
  isHarness,
13
- resolveSessionHarness
23
+ isModelCompatibleWithHarness,
24
+ modelProvider,
25
+ resolveSessionHarness,
26
+ snapHarnessToModel,
27
+ snapModelToHarness
14
28
  };
15
29
  //# sourceMappingURL=index.js.map
package/dist/index.d.ts CHANGED
@@ -9,7 +9,7 @@ export { AgentRuntime, AgentRuntimeModelConfig, AgentTurnOptions, AnySurfaceKind
9
9
  export { createTokenRecallChecker, producedFromToolEvents } from './eval/index.js';
10
10
  export { KnowledgeRequirementSpec, KnowledgeSignal, KnowledgeStateAccessor, SatisfiedByRule, buildKnowledgeRequirements, deriveSignals } from './knowledge/index.js';
11
11
  export { CreateKnowledgeLoopDeps, KnowledgeCandidate, KnowledgeDecider, KnowledgeDeciderInput, KnowledgeDecision, KnowledgeGateVerdict, KnowledgeLoop, KnowledgeLoopDriver, createKnowledgeLoop, createReviewerDecider, reviewCandidate } from './knowledge-loop/index.js';
12
- export { DEFAULT_HARNESS, Harness, KNOWN_HARNESSES, ResolveSessionHarnessInput, ResolvedSessionHarness, coerceHarness, isHarness, resolveSessionHarness } from './harness/index.js';
12
+ export { DEFAULT_HARNESS, HARNESS_MODEL_POLICIES, Harness, HarnessModelPolicy, KNOWN_HARNESSES, PROVIDER_PREFERRED_HARNESS, ResolveSessionHarnessInput, ResolvedSessionHarness, assertHarnessModelCompatible, coerceHarness, isHarness, isModelCompatibleWithHarness, modelProvider, resolveSessionHarness, snapHarnessToModel, snapModelToHarness } from './harness/index.js';
13
13
  export { AgentAppConfig, AgentDelegationConfig, AgentIdentityConfig, AgentIntegrationsConfig, AgentKnowledgeConfig, AgentTaxonomyConfig, AgentUiConfig, KnowledgeLoopConfig, KnowledgeSourceSpec, agentAppConfigJsonSchema, defineAgentApp } from './config/index.js';
14
14
  export { D1Like, D1PreparedLike, DrizzleColumnLike, DrizzleSqliteCoreLike, PRESET_MIGRATION_SQL, PRESET_TABLES, PresetBillingOptions, PresetKnowledgeAccessorOptions, PresetToolHandlerOptions, VaultKv, createD1KnowledgeStateAccessor, createPresetDrizzleSchema, createPresetFieldCrypto, createPresetToolHandlers, createPresetWorkspaceKeyManager, createPresetWorkspaceKeyStore } from './preset-cloudflare/index.js';
15
15
  export { KeyCrypto, KeyProvisioner, PlanLimit, PlatformBalanceInfo, PlatformBalanceManager, PlatformBalanceManagerOptions, PlatformBillingClient, PlatformIdentity, PlatformProductUsage, SharedBillingState, TcloudKeyClient, WorkspaceKeyManager, WorkspaceKeyManagerOptions, WorkspaceKeyRecord, WorkspaceKeyStore, WorkspaceModelKeyUsage, createPlatformBalanceManager, createTcloudKeyProvisioner, createWorkspaceKeyManager } from './billing/index.js';
package/dist/index.js CHANGED
@@ -236,11 +236,18 @@ import {
236
236
  } from "./chunk-EEPJGZJW.js";
237
237
  import {
238
238
  DEFAULT_HARNESS,
239
+ HARNESS_MODEL_POLICIES,
239
240
  KNOWN_HARNESSES,
241
+ PROVIDER_PREFERRED_HARNESS,
242
+ assertHarnessModelCompatible,
240
243
  coerceHarness,
241
244
  isHarness,
242
- resolveSessionHarness
243
- } from "./chunk-SD2H4FWY.js";
245
+ isModelCompatibleWithHarness,
246
+ modelProvider,
247
+ resolveSessionHarness,
248
+ snapHarnessToModel,
249
+ snapModelToHarness
250
+ } from "./chunk-5VXPDXZJ.js";
244
251
  import {
245
252
  createCapabilityToken,
246
253
  createExpiringCapabilityToken,
@@ -342,6 +349,7 @@ export {
342
349
  DELEGATION_TOOLS,
343
350
  EXPORT_PRESETS,
344
351
  EmailContentSchema,
352
+ HARNESS_MODEL_POLICIES,
345
353
  HubExecClient,
346
354
  ImageContentSchema,
347
355
  KNOWN_HARNESSES,
@@ -352,6 +360,7 @@ export {
352
360
  MissionConcurrencyError,
353
361
  PRESET_MIGRATION_SQL,
354
362
  PRESET_TABLES,
363
+ PROVIDER_PREFERRED_HARNESS,
355
364
  RetryableStepError,
356
365
  SCENE_ELEMENT_KINDS,
357
366
  SCENE_OPERATION_TYPES,
@@ -382,6 +391,7 @@ export {
382
391
  assertClipFitsSequence,
383
392
  assertColor,
384
393
  assertFinite,
394
+ assertHarnessModelCompatible,
385
395
  assertPositiveFinite,
386
396
  assertSceneMediaSrc,
387
397
  assertSequenceMediaUrl,
@@ -493,6 +503,7 @@ export {
493
503
  isHarness,
494
504
  isMissionStopRequested,
495
505
  isMissionTerminal,
506
+ isModelCompatibleWithHarness,
496
507
  isTangleBillingEnforcementDisabled,
497
508
  isTangleExecutionKeyError,
498
509
  lastClipEndFrame,
@@ -504,6 +515,7 @@ export {
504
515
  mergePersistedPart,
505
516
  mergeSurfaceOverlay,
506
517
  messageHasTurnId,
518
+ modelProvider,
507
519
  noopEventSink,
508
520
  normalizeClientTurnId,
509
521
  normalizeLanguageTag,
@@ -553,6 +565,8 @@ export {
553
565
  scalePageForChannelPreset,
554
566
  secondsToFrames,
555
567
  serializeCookie,
568
+ snapHarnessToModel,
569
+ snapModelToHarness,
556
570
  snapshotFrame,
557
571
  stepActivityFlowTrace,
558
572
  stepAgentActivity,
package/dist/run/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  KNOWN_HARNESSES
3
- } from "../chunk-SD2H4FWY.js";
3
+ } from "../chunk-5VXPDXZJ.js";
4
4
 
5
5
  // src/run/index.ts
6
6
  var ROUTER_HARNESS = "router";
@@ -1,4 +1,5 @@
1
- import { AgentProfileFileMount, AgentProfileMcpServer, AgentProfile, ScopedTokenScope, SandboxInstance, Sandbox } from '@tangle-network/sandbox';
1
+ import { AgentProfileFileMount, AgentProfileMcpServer, AgentProfile, StorageConfig, SandboxInstance, ScopedTokenScope, PromptResult, Sandbox } from '@tangle-network/sandbox';
2
+ export { StorageConfig } from '@tangle-network/sandbox';
2
3
  import { a as AppToolName, c as ToolHeaderNames } from '../auth-BlS9GWfL.js';
3
4
  import { b as AppToolContext } from '../types-2rOJo8Hc.js';
4
5
  import { Harness } from '../harness/index.js';
@@ -33,6 +34,21 @@ interface ProviderResolutionConfig {
33
34
  interface SandboxBuildContext {
34
35
  workspaceId: string;
35
36
  connectedIntegrationIds: string[];
37
+ userId?: string;
38
+ }
39
+
40
+ interface SandboxScope {
41
+ workspaceId: string;
42
+ userId?: string;
43
+ }
44
+ interface SandboxRestoreSpec {
45
+ fromSnapshot: string;
46
+ fromSandboxId: string;
47
+ }
48
+ interface LivenessProbeConfig {
49
+ sidecarProcessPattern: (harness: Harness) => string;
50
+ execTimeoutMs?: number;
51
+ psTimeoutMs?: number;
36
52
  }
37
53
  interface ProfileComposeOptions {
38
54
  systemPrompt?: string;
@@ -41,7 +57,7 @@ interface ProfileComposeOptions {
41
57
  name?: string;
42
58
  }
43
59
  interface SandboxRuntimeConfig {
44
- credentials: () => SandboxClientCredentials | null;
60
+ credentials: (scope?: SandboxScope) => SandboxClientCredentials | null | Promise<SandboxClientCredentials | null>;
45
61
  name: (workspaceId: string) => string;
46
62
  metadata: (harness: Harness) => Record<string, unknown>;
47
63
  connectedIntegrationIds: (workspaceId: string) => Promise<string[]>;
@@ -52,6 +68,14 @@ interface SandboxRuntimeConfig {
52
68
  permissionRole?: (workspaceRole: string) => SandboxPermissionLevel;
53
69
  resources?: SandboxResourceConfig;
54
70
  provider?: ProviderResolutionConfig;
71
+ storage?: (ctx: SandboxBuildContext) => StorageConfig | undefined;
72
+ restore?: (ctx: SandboxBuildContext) => SandboxRestoreSpec | undefined;
73
+ boxKey?: (scope: SandboxScope) => string;
74
+ childKeyMint?: (scope: SandboxScope) => Promise<Outcome<string>>;
75
+ bootstrap?: (box: SandboxInstance, scope: SandboxScope) => Promise<Outcome<void>>;
76
+ livenessProbe?: LivenessProbeConfig;
77
+ resumeStopped?: boolean;
78
+ backendModelAtCreate?: boolean;
55
79
  }
56
80
  declare const DEFAULT_SANDBOX_RESOURCES: SandboxResourceConfig;
57
81
  declare function getClient(shell: SandboxRuntimeConfig): Sandbox;
@@ -73,6 +97,7 @@ interface EnsureWorkspaceSandboxOptions {
73
97
  workspaceId: string;
74
98
  userId?: string;
75
99
  harness: Harness;
100
+ forceNew?: boolean;
76
101
  }
77
102
  declare function ensureWorkspaceSandbox(shell: SandboxRuntimeConfig, options: EnsureWorkspaceSandboxOptions): Promise<SandboxInstance>;
78
103
  interface ResolvedModel {
@@ -107,6 +132,9 @@ interface StreamSandboxPromptOptions {
107
132
  appToolMcp?: Record<string, AgentProfileMcpServer>;
108
133
  baseProfileMcp?: Record<string, AgentProfileMcpServer>;
109
134
  extraMcp?: Record<string, AgentProfileMcpServer>;
135
+ signal?: AbortSignal;
136
+ timeoutMs?: number;
137
+ disallowQuestions?: boolean;
110
138
  }
111
139
  declare function streamSandboxPrompt(shell: SandboxRuntimeConfig, box: SandboxInstance, message: string, options?: StreamSandboxPromptOptions): AsyncGenerator<unknown>;
112
140
  declare function runSandboxPrompt(shell: SandboxRuntimeConfig, box: SandboxInstance, message: string, options?: StreamSandboxPromptOptions): Promise<string>;
@@ -142,5 +170,28 @@ declare function mintSandboxScopedToken(box: SandboxInstance, options: {
142
170
  sessionId?: string;
143
171
  ttlMinutes?: number;
144
172
  }): Promise<Outcome<ScopedTokenResult>>;
173
+ declare function driveSandboxTurn(shell: SandboxRuntimeConfig, box: SandboxInstance, message: string, options: StreamSandboxPromptOptions & {
174
+ sessionId: string;
175
+ }): Promise<Outcome<PromptResult>>;
176
+ interface TerminalProxyIdentity {
177
+ userId: string;
178
+ workspaceId: string;
179
+ sandboxId: string;
180
+ }
181
+ declare function mintTerminalProxyToken(secret: string, identity: TerminalProxyIdentity, ttlMs?: number): Promise<Outcome<{
182
+ token: string;
183
+ expiresAt: Date;
184
+ }>>;
185
+ declare function verifyTerminalProxyToken(secret: string, token: string, expected: TerminalProxyIdentity): Promise<boolean>;
186
+ type SandboxStepTransition = {
187
+ kind: 'step-start';
188
+ } | {
189
+ kind: 'step-finish';
190
+ reason: string;
191
+ severed: boolean;
192
+ };
193
+ declare function classifySeveredStream(event: unknown): SandboxStepTransition | null;
194
+ declare function isTerminalPromptEvent(event: unknown): boolean;
195
+ declare function detectInteractiveQuestion(event: unknown): string | null;
145
196
 
146
- export { type AppToolDescriptor, type BuildAppToolMcpServersOptions, DEFAULT_SANDBOX_RESOURCES, type EnsureWorkspaceSandboxOptions, type MemberSyncSeam, type Outcome, type ProfileComposeOptions, type ProviderResolutionConfig, type ResolvedModel, type SandboxBuildContext, type SandboxClientCredentials, type SandboxPermissionLevel, type SandboxResourceConfig, type SandboxRuntimeConfig, type ScopedTokenResult, type SecretStore, type StreamSandboxPromptOptions, attachReasoningEffort, buildAppToolMcpServers, deleteSecret, ensureWorkspaceSandbox, flattenHistory, getClient, mergeExtraMcp, mintSandboxScopedToken, readSecret, resetClientCache, resolveModel, runSandboxPrompt, secretStoreFromClient, storeSecret, streamSandboxPrompt, syncSandboxMemberAdd, syncSandboxMemberRemove, syncSandboxMemberRole };
197
+ export { type AppToolDescriptor, type BuildAppToolMcpServersOptions, DEFAULT_SANDBOX_RESOURCES, type EnsureWorkspaceSandboxOptions, type LivenessProbeConfig, type MemberSyncSeam, type Outcome, type ProfileComposeOptions, type ProviderResolutionConfig, type ResolvedModel, type SandboxBuildContext, type SandboxClientCredentials, type SandboxPermissionLevel, type SandboxResourceConfig, type SandboxRestoreSpec, type SandboxRuntimeConfig, type SandboxScope, type SandboxStepTransition, type ScopedTokenResult, type SecretStore, type StreamSandboxPromptOptions, type TerminalProxyIdentity, attachReasoningEffort, buildAppToolMcpServers, classifySeveredStream, deleteSecret, detectInteractiveQuestion, driveSandboxTurn, ensureWorkspaceSandbox, flattenHistory, getClient, isTerminalPromptEvent, mergeExtraMcp, mintSandboxScopedToken, mintTerminalProxyToken, readSecret, resetClientCache, resolveModel, runSandboxPrompt, secretStoreFromClient, storeSecret, streamSandboxPrompt, syncSandboxMemberAdd, syncSandboxMemberRemove, syncSandboxMemberRole, verifyTerminalProxyToken };
@@ -1,3 +1,6 @@
1
+ import {
2
+ assertHarnessModelCompatible
3
+ } from "../chunk-5VXPDXZJ.js";
1
4
  import "../chunk-MH6AVXQ7.js";
2
5
  import {
3
6
  buildAppToolMcpServer
@@ -22,15 +25,21 @@ var DEFAULT_SANDBOX_RESOURCES = {
22
25
  idleTimeoutSeconds: 3600
23
26
  };
24
27
  var _cached = null;
25
- function getClient(shell) {
26
- const creds = shell.credentials();
27
- if (!creds) throw new Error("sandbox credentials are required (apiKey/baseUrl)");
28
+ function getClientFromCreds(creds) {
28
29
  const fingerprint = `${creds.apiKey} ${creds.baseUrl}`;
29
30
  if (_cached && _cached.fingerprint === fingerprint) return _cached.client;
30
31
  const client = new Sandbox({ apiKey: creds.apiKey, baseUrl: creds.baseUrl });
31
32
  _cached = { client, fingerprint };
32
33
  return client;
33
34
  }
35
+ function getClient(shell) {
36
+ const creds = shell.credentials();
37
+ if (creds && typeof creds.then === "function") {
38
+ throw new Error("getClient: scoped (async) credentials require the async sandbox path");
39
+ }
40
+ if (!creds) throw new Error("sandbox credentials are required (apiKey/baseUrl)");
41
+ return getClientFromCreds(creds);
42
+ }
34
43
  function resetClientCache() {
35
44
  _cached = null;
36
45
  }
@@ -48,6 +57,9 @@ function buildAppToolMcpServers(options) {
48
57
  }
49
58
  return entries;
50
59
  }
60
+ function shellSingleQuote(value) {
61
+ return `'${value.replace(/'/g, `'\\''`)}'`;
62
+ }
51
63
  async function listRunning(client, name) {
52
64
  try {
53
65
  const running = await client.list({ status: "running" });
@@ -64,25 +76,98 @@ async function deleteBox(box) {
64
76
  return fail(err);
65
77
  }
66
78
  }
79
+ async function isBoxAlive(box, harness, probe) {
80
+ if (!probe) return true;
81
+ const execTimeout = probe.execTimeoutMs ?? 5e3;
82
+ const psTimeout = probe.psTimeoutMs ?? 3e3;
83
+ const race = (p, ms, label) => Promise.race([
84
+ p,
85
+ new Promise((_, reject) => setTimeout(() => reject(new Error(label)), ms))
86
+ ]);
87
+ try {
88
+ const alive = await race(box.exec("echo alive"), execTimeout, "alive check timeout");
89
+ if (!alive.stdout.includes("alive")) return false;
90
+ const pattern = probe.sidecarProcessPattern(harness);
91
+ try {
92
+ const ps = await race(
93
+ box.exec(`pgrep -f ${shellSingleQuote(pattern)} || echo no-sidecar`),
94
+ psTimeout,
95
+ "ps check timeout"
96
+ );
97
+ if (ps.stdout.includes("no-sidecar")) return false;
98
+ } catch {
99
+ }
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+ async function resumeStoppedBox(client, name, timeoutMs) {
106
+ try {
107
+ const stopped = await client.list({ status: "stopped" });
108
+ const match = stopped.find((s) => s.name === name) ?? null;
109
+ if (!match) return ok(null);
110
+ await match.resume();
111
+ await match.waitFor("running", { timeoutMs });
112
+ return ok(match);
113
+ } catch (err) {
114
+ return fail(err);
115
+ }
116
+ }
67
117
  async function ensureWorkspaceSandbox(shell, options) {
68
- const { workspaceId, userId, harness } = options;
69
- const client = getClient(shell);
70
- const name = shell.name(workspaceId);
118
+ const { workspaceId, userId, harness, forceNew } = options;
119
+ const scope = { workspaceId, ...userId ? { userId } : {} };
120
+ const creds = await shell.credentials(scope);
121
+ if (!creds) throw new Error("sandbox credentials are required (apiKey/baseUrl)");
122
+ const client = getClientFromCreds(creds);
123
+ const name = shell.boxKey ? shell.boxKey(scope) : shell.name(workspaceId);
71
124
  const resources = shell.resources ?? DEFAULT_SANDBOX_RESOURCES;
125
+ const resumeTimeout = 12e4;
72
126
  const existing = await listRunning(client, name);
73
127
  if (existing.succeeded && existing.value) {
74
128
  const found = existing.value;
75
- if (found.metadata?.harness === harness) return found;
76
- const dropped = await deleteBox(found);
77
- if (!dropped.succeeded) {
78
- throw new Error(
79
- `harness-mismatched sandbox ${name} (was ${String(found.metadata?.harness ?? "unknown")}, want ${harness}) could not be deleted`,
80
- { cause: dropped.error }
81
- );
129
+ if (forceNew) {
130
+ const dropped = await deleteBox(found);
131
+ if (!dropped.succeeded) {
132
+ throw new Error(`forceNew: sandbox ${name} could not be deleted`, { cause: dropped.error });
133
+ }
134
+ } else if (found.metadata?.harness === harness && await isBoxAlive(found, harness, shell.livenessProbe)) {
135
+ if (shell.bootstrap) {
136
+ const boot = await shell.bootstrap(found, scope);
137
+ if (!boot.succeeded) {
138
+ throw new Error(`bootstrap failed on reused box ${name}`, { cause: boot.error });
139
+ }
140
+ }
141
+ return found;
142
+ } else {
143
+ const dropped = await deleteBox(found);
144
+ if (!dropped.succeeded) {
145
+ throw new Error(
146
+ `sandbox ${name} (was ${String(found.metadata?.harness ?? "unknown")}, want ${harness}, or unresponsive) could not be deleted`,
147
+ { cause: dropped.error }
148
+ );
149
+ }
150
+ }
151
+ }
152
+ if (!forceNew && shell.resumeStopped !== false) {
153
+ const resumed = await resumeStoppedBox(client, name, resumeTimeout);
154
+ if (resumed.succeeded && resumed.value && await isBoxAlive(resumed.value, harness, shell.livenessProbe)) {
155
+ const box2 = resumed.value;
156
+ if (shell.bootstrap) {
157
+ const boot = await shell.bootstrap(box2, scope);
158
+ if (!boot.succeeded) {
159
+ throw new Error(`bootstrap failed on resumed box ${name}`, { cause: boot.error });
160
+ }
161
+ }
162
+ return box2;
82
163
  }
83
164
  }
84
165
  const connectedIntegrationIds = await shell.connectedIntegrationIds(workspaceId);
85
- const buildCtx = { workspaceId, connectedIntegrationIds };
166
+ const buildCtx = {
167
+ workspaceId,
168
+ connectedIntegrationIds,
169
+ ...userId ? { userId } : {}
170
+ };
86
171
  const [secrets, env, files] = await Promise.all([
87
172
  shell.secrets(workspaceId),
88
173
  shell.env(buildCtx),
@@ -90,6 +175,19 @@ async function ensureWorkspaceSandbox(shell, options) {
90
175
  ]);
91
176
  const profile = shell.profile({ extraFiles: files });
92
177
  const role = userId && shell.permissionRole ? shell.permissionRole("developer") : void 0;
178
+ let model = shell.backendModelAtCreate ? resolveModel(shell.provider) : void 0;
179
+ if (model && shell.childKeyMint && model.provider === "openai-compat") {
180
+ const minted = await shell.childKeyMint(scope);
181
+ if (minted.succeeded) model = { ...model, apiKey: minted.value };
182
+ else {
183
+ console.error(
184
+ `[sandbox] childKeyMint failed for ${workspaceId}; using parent key:`,
185
+ minted.error.message
186
+ );
187
+ }
188
+ }
189
+ const storage = shell.storage?.(buildCtx);
190
+ const restore = shell.restore?.(buildCtx);
93
191
  const payload = {
94
192
  name,
95
193
  image: resources.image,
@@ -97,7 +195,9 @@ async function ensureWorkspaceSandbox(shell, options) {
97
195
  ...userId ? { permissions: { initialUsers: [{ userId, role }] } } : {},
98
196
  env,
99
197
  secrets,
100
- backend: { type: harness, profile },
198
+ backend: { type: harness, profile, ...model ? { model } : {} },
199
+ ...storage ? { storage } : {},
200
+ ...restore ? restore : {},
101
201
  maxLifetimeSeconds: resources.maxLifetimeSeconds,
102
202
  idleTimeoutSeconds: resources.idleTimeoutSeconds,
103
203
  resources: {
@@ -109,6 +209,12 @@ async function ensureWorkspaceSandbox(shell, options) {
109
209
  const box = await client.create(payload);
110
210
  await box.waitFor("running", { timeoutMs: 12e4 });
111
211
  if (!box.connection?.runtimeUrl) await box.refresh();
212
+ if (shell.bootstrap) {
213
+ const boot = await shell.bootstrap(box, scope);
214
+ if (!boot.succeeded) {
215
+ throw new Error(`bootstrap failed on new box ${name}`, { cause: boot.error });
216
+ }
217
+ }
112
218
  return box;
113
219
  }
114
220
  function resolveModel(config, override) {
@@ -160,6 +266,7 @@ async function* streamSandboxPrompt(shell, box, message, options) {
160
266
  model: options?.model,
161
267
  modelApiKey: options?.modelApiKey
162
268
  });
269
+ if (model?.model) assertHarnessModelCompatible(harness, model.model);
163
270
  const prompt = flattenHistory(message, options?.history);
164
271
  const appToolMcp = options?.appToolMcp ?? {};
165
272
  const extraMcp = mergeExtraMcp(appToolMcp, options?.baseProfileMcp ?? {}, options?.extraMcp);
@@ -169,13 +276,32 @@ async function* streamSandboxPrompt(shell, box, message, options) {
169
276
  sessionId: options?.sessionId,
170
277
  executionId: options?.executionId,
171
278
  lastEventId: options?.lastEventId,
279
+ ...options?.signal ? { signal: options.signal } : {},
280
+ ...options?.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
172
281
  backend: {
173
282
  type: harness,
174
283
  profile: profileWithEffort,
175
284
  ...model ? { model } : {}
176
285
  }
177
286
  });
178
- for await (const event of stream) yield event;
287
+ let severedFinishReason = null;
288
+ for await (const event of stream) {
289
+ const step = classifySeveredStream(event);
290
+ if (step) severedFinishReason = step.kind === "step-finish" && step.severed ? step.reason : null;
291
+ if (severedFinishReason && isTerminalPromptEvent(event)) {
292
+ throw new Error(`sandbox model stream severed mid-turn (reason="${severedFinishReason}")`);
293
+ }
294
+ if (options?.disallowQuestions) {
295
+ const q = detectInteractiveQuestion(event);
296
+ if (q) {
297
+ throw new Error(`sandbox agent asked an interactive question during an autonomous run: ${q}`);
298
+ }
299
+ }
300
+ yield event;
301
+ }
302
+ if (severedFinishReason) {
303
+ throw new Error(`sandbox model stream severed mid-turn (reason="${severedFinishReason}")`);
304
+ }
179
305
  }
180
306
  async function runSandboxPrompt(shell, box, message, options) {
181
307
  let fullText = "";
@@ -280,16 +406,148 @@ async function mintSandboxScopedToken(box, options) {
280
406
  return fail(err);
281
407
  }
282
408
  }
409
+ async function driveSandboxTurn(shell, box, message, options) {
410
+ const harness = options.harness ?? "opencode";
411
+ const model = resolveModel(shell.provider, {
412
+ model: options.model,
413
+ modelApiKey: options.modelApiKey
414
+ });
415
+ if (model?.model) assertHarnessModelCompatible(harness, model.model);
416
+ const prompt = flattenHistory(message, options.history);
417
+ const appToolMcp = options.appToolMcp ?? {};
418
+ const extraMcp = mergeExtraMcp(appToolMcp, options.baseProfileMcp ?? {}, options.extraMcp);
419
+ const profile = attachReasoningEffort(
420
+ shell.profile({ systemPrompt: options.systemPrompt, extraMcp }),
421
+ harness,
422
+ options.effort
423
+ );
424
+ try {
425
+ const result = await box.prompt(prompt, {
426
+ sessionId: options.sessionId,
427
+ ...options.executionId ? { executionId: options.executionId } : {},
428
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
429
+ ...options.signal ? { signal: options.signal } : {},
430
+ backend: { type: harness, profile, ...model ? { model } : {} }
431
+ });
432
+ if (!result.success) return fail(new Error(result.error ?? "sandbox turn failed"));
433
+ return ok(result);
434
+ } catch (err) {
435
+ return fail(err);
436
+ }
437
+ }
438
+ var TERMINAL_PROXY_TOKEN_TTL_MS = 15 * 60 * 1e3;
439
+ async function signTerminalProxyToken(secret, encodedPayload) {
440
+ const key = await crypto.subtle.importKey(
441
+ "raw",
442
+ new TextEncoder().encode(secret),
443
+ { name: "HMAC", hash: "SHA-256" },
444
+ false,
445
+ ["sign"]
446
+ );
447
+ const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(encodedPayload));
448
+ return base64UrlEncodeBytes(new Uint8Array(sig));
449
+ }
450
+ async function mintTerminalProxyToken(secret, identity, ttlMs = TERMINAL_PROXY_TOKEN_TTL_MS) {
451
+ if (!secret) return fail(new Error("mintTerminalProxyToken: secret is required"));
452
+ if (!identity.userId || !identity.workspaceId || !identity.sandboxId) {
453
+ return fail(new Error("mintTerminalProxyToken: userId/workspaceId/sandboxId are required"));
454
+ }
455
+ const expiresAt = new Date(Date.now() + ttlMs);
456
+ const payload = { ...identity, exp: Math.floor(expiresAt.getTime() / 1e3) };
457
+ const encoded = base64UrlEncodeUtf8(JSON.stringify(payload));
458
+ const sig = await signTerminalProxyToken(secret, encoded);
459
+ return ok({ token: `${encoded}.${sig}`, expiresAt });
460
+ }
461
+ async function verifyTerminalProxyToken(secret, token, expected) {
462
+ if (!secret) return false;
463
+ const [encoded, sig, extra] = token.split(".");
464
+ if (!encoded || !sig || extra !== void 0) return false;
465
+ const expectedSig = await signTerminalProxyToken(secret, encoded);
466
+ if (!constantTimeEqual(sig, expectedSig)) return false;
467
+ let payload;
468
+ try {
469
+ payload = JSON.parse(base64UrlDecodeUtf8(encoded));
470
+ } catch {
471
+ return false;
472
+ }
473
+ return payload.userId === expected.userId && payload.workspaceId === expected.workspaceId && payload.sandboxId === expected.sandboxId && Number.isFinite(payload.exp) && payload.exp > Math.floor(Date.now() / 1e3);
474
+ }
475
+ function base64UrlEncodeBytes(bytes) {
476
+ let bin = "";
477
+ for (const b of bytes) bin += String.fromCharCode(b);
478
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
479
+ }
480
+ function base64UrlEncodeUtf8(v) {
481
+ return base64UrlEncodeBytes(new TextEncoder().encode(v));
482
+ }
483
+ function base64UrlDecodeUtf8(v) {
484
+ const padded = v.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(v.length / 4) * 4, "=");
485
+ const bin = atob(padded);
486
+ const bytes = new Uint8Array(bin.length);
487
+ for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i);
488
+ return new TextDecoder().decode(bytes);
489
+ }
490
+ function constantTimeEqual(a, b) {
491
+ if (a.length !== b.length) return false;
492
+ let r = 0;
493
+ for (let i = 0; i < a.length; i += 1) r |= a.charCodeAt(i) ^ b.charCodeAt(i);
494
+ return r === 0;
495
+ }
496
+ var SEVERED_FINISH_REASONS = /* @__PURE__ */ new Set(["error", "other", "unknown"]);
497
+ function asPlainRecord(v) {
498
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
499
+ }
500
+ function classifySeveredStream(event) {
501
+ const root = asPlainRecord(event);
502
+ if (!root || root.type !== "message.part.updated") return null;
503
+ const body = asPlainRecord(root.properties) ?? asPlainRecord(root.data) ?? root;
504
+ const part = asPlainRecord(body.part);
505
+ if (!part) return null;
506
+ if (part.type === "step-start") return { kind: "step-start" };
507
+ if (part.type !== "step-finish") return null;
508
+ const reason = typeof part.reason === "string" && part.reason ? part.reason : "unknown";
509
+ return { kind: "step-finish", reason, severed: SEVERED_FINISH_REASONS.has(reason) };
510
+ }
511
+ function isTerminalPromptEvent(event) {
512
+ const t = asPlainRecord(event)?.type;
513
+ return t === "result" || t === "done";
514
+ }
515
+ function detectInteractiveQuestion(event) {
516
+ const root = asPlainRecord(event);
517
+ if (!root) return null;
518
+ const type = typeof root.type === "string" ? root.type : void 0;
519
+ const data = asPlainRecord(root.data);
520
+ const props = asPlainRecord(root.properties);
521
+ const body = props ?? data ?? root;
522
+ if (type === "question.asked" || type === "question") return firstQuestionText(body);
523
+ const part = asPlainRecord(data?.part) ?? asPlainRecord(body.part);
524
+ const tool = typeof part?.tool === "string" && part.tool || typeof part?.name === "string" && part.name || typeof body.tool === "string" && body.tool || void 0;
525
+ const isQ = type === "message.part.updated" && (tool === "question" || asPlainRecord(part)?.type === "question");
526
+ if (!isQ) return null;
527
+ const state = asPlainRecord(asPlainRecord(part)?.state);
528
+ return firstQuestionText(asPlainRecord(state?.input) ?? state ?? part ?? body);
529
+ }
530
+ function firstQuestionText(value) {
531
+ const arr = Array.isArray(value?.questions) ? value.questions : Array.isArray(asPlainRecord(value?.input)?.questions) ? asPlainRecord(value.input).questions : [];
532
+ const first = asPlainRecord(arr[0]);
533
+ const q = typeof first?.question === "string" && first.question || typeof first?.prompt === "string" && first.prompt || void 0;
534
+ return q ?? "interactive question";
535
+ }
283
536
  export {
284
537
  DEFAULT_SANDBOX_RESOURCES,
285
538
  attachReasoningEffort,
286
539
  buildAppToolMcpServers,
540
+ classifySeveredStream,
287
541
  deleteSecret,
542
+ detectInteractiveQuestion,
543
+ driveSandboxTurn,
288
544
  ensureWorkspaceSandbox,
289
545
  flattenHistory,
290
546
  getClient,
547
+ isTerminalPromptEvent,
291
548
  mergeExtraMcp,
292
549
  mintSandboxScopedToken,
550
+ mintTerminalProxyToken,
293
551
  readSecret,
294
552
  resetClientCache,
295
553
  resolveModel,
@@ -299,6 +557,7 @@ export {
299
557
  streamSandboxPrompt,
300
558
  syncSandboxMemberAdd,
301
559
  syncSandboxMemberRemove,
302
- syncSandboxMemberRole
560
+ syncSandboxMemberRole,
561
+ verifyTerminalProxyToken
303
562
  };
304
563
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/sandbox/index.ts"],"sourcesContent":["import {\n Sandbox,\n type AgentProfile,\n type AgentProfileFileMount,\n type AgentProfileMcpServer,\n type SandboxInstance,\n type ScopedTokenScope,\n} from '@tangle-network/sandbox'\nimport {\n buildAppToolMcpServer,\n type AppToolName,\n type AppToolContext,\n type ToolHeaderNames,\n} from '../tools/index'\nimport type { Harness } from '../harness/index'\n\nexport type Outcome<T> =\n | { succeeded: true; value: T }\n | { succeeded: false; error: Error }\n\nconst ok = <T>(value: T): Outcome<T> => ({ succeeded: true, value })\nconst fail = (error: unknown): Outcome<never> => ({\n succeeded: false,\n error: error instanceof Error ? error : new Error(String(error)),\n})\n\nexport interface SandboxClientCredentials {\n apiKey: string\n baseUrl: string\n}\n\nexport interface SandboxResourceConfig {\n image: string\n cpuCores: number\n memoryMB: number\n diskGB: number\n maxLifetimeSeconds: number\n idleTimeoutSeconds: number\n}\n\nexport interface ProviderResolutionConfig {\n routerBaseUrl?: string\n apiKey?: string\n providerName?: string\n modelName?: string\n defaultModel?: string\n openaiApiKey?: string\n}\n\nexport interface SandboxBuildContext {\n workspaceId: string\n connectedIntegrationIds: string[]\n}\n\nexport interface ProfileComposeOptions {\n systemPrompt?: string\n extraFiles?: AgentProfileFileMount[]\n extraMcp?: Record<string, AgentProfileMcpServer>\n name?: string\n}\n\nexport interface SandboxRuntimeConfig {\n credentials: () => SandboxClientCredentials | null\n name: (workspaceId: string) => string\n metadata: (harness: Harness) => Record<string, unknown>\n connectedIntegrationIds: (workspaceId: string) => Promise<string[]>\n env: (ctx: SandboxBuildContext) => Promise<Record<string, string>>\n files: (ctx: SandboxBuildContext) => Promise<AgentProfileFileMount[]>\n secrets: (workspaceId: string) => Promise<string[]>\n profile: (options: ProfileComposeOptions) => AgentProfile\n permissionRole?: (workspaceRole: string) => SandboxPermissionLevel\n resources?: SandboxResourceConfig\n provider?: ProviderResolutionConfig\n}\n\nexport const DEFAULT_SANDBOX_RESOURCES: SandboxResourceConfig = {\n image: 'universal',\n cpuCores: 2,\n memoryMB: 4096,\n diskGB: 10,\n maxLifetimeSeconds: 86400,\n idleTimeoutSeconds: 3600,\n}\n\ninterface ClientCacheEntry {\n client: Sandbox\n fingerprint: string\n}\n\nlet _cached: ClientCacheEntry | null = null\n\nexport function getClient(shell: SandboxRuntimeConfig): Sandbox {\n const creds = shell.credentials()\n if (!creds) throw new Error('sandbox credentials are required (apiKey/baseUrl)')\n\n const fingerprint = `${creds.apiKey} ${creds.baseUrl}`\n if (_cached && _cached.fingerprint === fingerprint) return _cached.client\n\n const client = new Sandbox({ apiKey: creds.apiKey, baseUrl: creds.baseUrl })\n _cached = { client, fingerprint }\n return client\n}\n\nexport function resetClientCache(): void {\n _cached = null\n}\n\nexport interface AppToolDescriptor {\n tool: AppToolName\n key: string\n description: string\n}\n\nexport interface BuildAppToolMcpServersOptions {\n tools: AppToolDescriptor[]\n baseUrl: string\n token: string\n ctx: AppToolContext\n headerNames?: ToolHeaderNames\n}\n\nexport function buildAppToolMcpServers(\n options: BuildAppToolMcpServersOptions,\n): Record<string, AgentProfileMcpServer> {\n const entries: Record<string, AgentProfileMcpServer> = {}\n for (const { tool, key, description } of options.tools) {\n entries[key] = buildAppToolMcpServer({\n tool,\n baseUrl: options.baseUrl,\n token: options.token,\n ctx: options.ctx,\n description,\n headerNames: options.headerNames,\n }) as AgentProfileMcpServer\n }\n return entries\n}\n\nexport interface EnsureWorkspaceSandboxOptions {\n workspaceId: string\n userId?: string\n harness: Harness\n}\n\nasync function listRunning(\n client: Sandbox,\n name: string,\n): Promise<Outcome<SandboxInstance | null>> {\n try {\n const running = await client.list({ status: 'running' })\n return ok(running.find((s) => s.name === name) ?? null)\n } catch (err) {\n return fail(err)\n }\n}\n\nasync function deleteBox(box: SandboxInstance): Promise<Outcome<void>> {\n try {\n await box.delete()\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\n// The SDK narrows `backend.type` to its own BackendType union and\n// `initialUsers[].role` to PermissionLevel — neither symbol is exported. The\n// create payload is assembled with the product's Harness/role strings, which\n// are a superset surface; the localized cast at the boundary is the only place\n// this widening is allowed, and the runtime contract (the sidecar boots the\n// named harness) is what enforces correctness.\ntype CreatePayload = Parameters<Sandbox['create']>[0]\n\nexport async function ensureWorkspaceSandbox(\n shell: SandboxRuntimeConfig,\n options: EnsureWorkspaceSandboxOptions,\n): Promise<SandboxInstance> {\n const { workspaceId, userId, harness } = options\n const client = getClient(shell)\n const name = shell.name(workspaceId)\n const resources = shell.resources ?? DEFAULT_SANDBOX_RESOURCES\n\n const existing = await listRunning(client, name)\n if (existing.succeeded && existing.value) {\n const found = existing.value\n if (found.metadata?.harness === harness) return found\n const dropped = await deleteBox(found)\n if (!dropped.succeeded) {\n throw new Error(\n `harness-mismatched sandbox ${name} ` +\n `(was ${String(found.metadata?.harness ?? 'unknown')}, want ${harness}) could not be deleted`,\n { cause: dropped.error },\n )\n }\n }\n\n const connectedIntegrationIds = await shell.connectedIntegrationIds(workspaceId)\n const buildCtx: SandboxBuildContext = { workspaceId, connectedIntegrationIds }\n const [secrets, env, files] = await Promise.all([\n shell.secrets(workspaceId),\n shell.env(buildCtx),\n shell.files(buildCtx),\n ])\n const profile = shell.profile({ extraFiles: files })\n\n const role = userId && shell.permissionRole ? shell.permissionRole('developer') : undefined\n\n const payload = {\n name,\n image: resources.image,\n metadata: shell.metadata(harness),\n ...(userId ? { permissions: { initialUsers: [{ userId, role }] } } : {}),\n env,\n secrets,\n backend: { type: harness, profile },\n maxLifetimeSeconds: resources.maxLifetimeSeconds,\n idleTimeoutSeconds: resources.idleTimeoutSeconds,\n resources: {\n cpuCores: resources.cpuCores,\n memoryMB: resources.memoryMB,\n diskGB: resources.diskGB,\n },\n } as CreatePayload\n\n const box = await client.create(payload)\n\n await box.waitFor('running', { timeoutMs: 120_000 })\n if (!box.connection?.runtimeUrl) await box.refresh()\n return box\n}\n\nexport interface ResolvedModel {\n model: string\n provider: string\n apiKey: string\n baseUrl?: string\n}\n\nexport function resolveModel(\n config: ProviderResolutionConfig | undefined,\n override?: { model?: string; modelApiKey?: string },\n): ResolvedModel | undefined {\n const c = config ?? {}\n const explicitBaseUrl = c.routerBaseUrl\n const explicitApiKey = override?.modelApiKey ?? c.apiKey\n const provider =\n c.providerName ?? (explicitApiKey ? 'openai-compat' : c.openaiApiKey ? 'openai' : undefined)\n const modelName =\n override?.model ??\n c.modelName ??\n (provider === 'openai' || provider === 'openai-compat' ? c.defaultModel : undefined)\n const apiKey = explicitApiKey ?? (provider === 'openai' ? c.openaiApiKey : undefined)\n if (!provider || !modelName || !apiKey) return undefined\n return {\n model: modelName,\n provider,\n apiKey,\n ...(explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}),\n }\n}\n\nexport function flattenHistory(\n message: string,\n history?: Array<{ role: 'user' | 'assistant'; content: string }>,\n): string {\n if (!history?.length) return message\n const transcript = history\n .map((entry) => `${entry.role === 'assistant' ? 'Assistant' : 'User'}: ${entry.content}`)\n .join('\\n\\n')\n return `${transcript}\\n\\nUser: ${message}`\n}\n\nexport function mergeExtraMcp(\n appToolMcp: Record<string, AgentProfileMcpServer>,\n baseProfileMcp: Record<string, AgentProfileMcpServer>,\n extra: Record<string, AgentProfileMcpServer> | undefined,\n): Record<string, AgentProfileMcpServer> {\n for (const key of Object.keys(extra ?? {})) {\n if (key in appToolMcp || key in baseProfileMcp) {\n throw new Error(`extraMcp key '${key}' collides with an existing profile MCP server`)\n }\n }\n return { ...appToolMcp, ...(extra ?? {}) }\n}\n\nexport function attachReasoningEffort(\n profile: AgentProfile,\n harness: Harness,\n effort: 'auto' | 'low' | 'medium' | 'high' | undefined,\n): AgentProfile {\n if (!effort || effort === 'auto') return profile\n return {\n ...profile,\n extensions: {\n ...(profile.extensions ?? {}),\n [harness]: {\n ...(profile.extensions?.[harness] ?? {}),\n reasoningEffort: effort,\n },\n },\n }\n}\n\nexport interface StreamSandboxPromptOptions {\n sessionId?: string\n executionId?: string\n lastEventId?: string\n systemPrompt?: string\n model?: string\n modelApiKey?: string\n history?: Array<{ role: 'user' | 'assistant'; content: string }>\n harness?: Harness\n effort?: 'auto' | 'low' | 'medium' | 'high'\n appToolMcp?: Record<string, AgentProfileMcpServer>\n baseProfileMcp?: Record<string, AgentProfileMcpServer>\n extraMcp?: Record<string, AgentProfileMcpServer>\n}\n\ntype StreamPromptOptions = Parameters<SandboxInstance['streamPrompt']>[1]\n\nexport async function* streamSandboxPrompt(\n shell: SandboxRuntimeConfig,\n box: SandboxInstance,\n message: string,\n options?: StreamSandboxPromptOptions,\n): AsyncGenerator<unknown> {\n const harness = options?.harness ?? 'opencode'\n const model = resolveModel(shell.provider, {\n model: options?.model,\n modelApiKey: options?.modelApiKey,\n })\n\n const prompt = flattenHistory(message, options?.history)\n\n const appToolMcp = options?.appToolMcp ?? {}\n const extraMcp = mergeExtraMcp(appToolMcp, options?.baseProfileMcp ?? {}, options?.extraMcp)\n\n const profile = shell.profile({ systemPrompt: options?.systemPrompt, extraMcp })\n const profileWithEffort = attachReasoningEffort(profile, harness, options?.effort)\n\n const stream = box.streamPrompt(prompt, {\n sessionId: options?.sessionId,\n executionId: options?.executionId,\n lastEventId: options?.lastEventId,\n backend: {\n type: harness,\n profile: profileWithEffort,\n ...(model ? { model } : {}),\n },\n } as StreamPromptOptions)\n\n for await (const event of stream) yield event\n}\n\nexport async function runSandboxPrompt(\n shell: SandboxRuntimeConfig,\n box: SandboxInstance,\n message: string,\n options?: StreamSandboxPromptOptions,\n): Promise<string> {\n let fullText = ''\n let firstTextSeen = false\n\n for await (const rawEvent of streamSandboxPrompt(shell, box, message, options)) {\n const event = rawEvent as { type?: string; data?: Record<string, unknown> }\n if (!event.type) continue\n\n if (event.type === 'message.part.updated') {\n const part = event.data?.part as Record<string, unknown> | undefined\n const delta = typeof event.data?.delta === 'string' ? event.data.delta : null\n if (String(part?.type ?? '') === 'text') {\n if (!firstTextSeen) {\n firstTextSeen = true\n continue\n }\n if (delta) fullText += delta\n else if (typeof part?.text === 'string') fullText = part.text\n }\n } else if (event.type === 'result') {\n const finalText = typeof event.data?.finalText === 'string' ? event.data.finalText : null\n if (finalText) fullText = finalText\n }\n }\n\n return fullText\n}\n\n// Mirrors the SDK's PermissionLevel union (not re-exported by\n// @tangle-network/sandbox). The product's role-mapping seam must produce one of\n// these; binding the seam's return type to the union makes a wrong mapping a\n// compile error rather than a runtime 400 from the orchestrator.\nexport type SandboxPermissionLevel = 'owner' | 'admin' | 'developer' | 'viewer'\n\nexport interface MemberSyncSeam {\n roleToSandboxRole: (workspaceRole: string) => SandboxPermissionLevel\n}\n\nexport async function syncSandboxMemberAdd(\n box: SandboxInstance,\n seam: MemberSyncSeam,\n userId: string,\n role: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.add({ userId, role: seam.roleToSandboxRole(role) })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function syncSandboxMemberRemove(\n box: SandboxInstance,\n userId: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.remove(userId, { preserveHomeDir: true })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function syncSandboxMemberRole(\n box: SandboxInstance,\n seam: MemberSyncSeam,\n userId: string,\n role: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.update(userId, { role: seam.roleToSandboxRole(role) })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport interface SecretStore {\n create: (name: string, value: string) => Promise<void>\n update: (name: string, value: string) => Promise<void>\n get: (name: string) => Promise<string>\n delete: (name: string) => Promise<void>\n}\n\nexport function secretStoreFromClient(shell: SandboxRuntimeConfig): SecretStore {\n const client = getClient(shell)\n return {\n create: async (name, value) => {\n await client.secrets.create(name, value)\n },\n update: async (name, value) => {\n await client.secrets.update(name, value)\n },\n get: (name) => client.secrets.get(name),\n delete: async (name) => {\n await client.secrets.delete(name)\n },\n }\n}\n\nexport async function storeSecret(\n store: SecretStore,\n name: string,\n value: string,\n): Promise<Outcome<void>> {\n try {\n await store.create(name, value)\n return ok(undefined)\n } catch {\n try {\n await store.update(name, value)\n return ok(undefined)\n } catch (err) {\n return fail(new Error(`Failed to store sandbox secret ${name}`, { cause: err }))\n }\n }\n}\n\nexport async function readSecret(store: SecretStore, name: string): Promise<Outcome<string>> {\n try {\n return ok(await store.get(name))\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function deleteSecret(store: SecretStore, name: string): Promise<Outcome<void>> {\n try {\n await store.delete(name)\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport interface ScopedTokenResult {\n token: string\n expiresAt: Date\n scope: ScopedTokenScope\n}\n\n/**\n * Mint a scoped token for an already-provisioned box (e.g. to hand a terminal\n * proxy a narrowed credential). Uses the SDK's native `box.mintScopedToken`,\n * which normalizes `expiresAt` to a Date — no hand-rolled wire call.\n */\nexport async function mintSandboxScopedToken(\n box: SandboxInstance,\n options: { scope: ScopedTokenScope; sessionId?: string; ttlMinutes?: number },\n): Promise<Outcome<ScopedTokenResult>> {\n try {\n const token = await box.mintScopedToken({\n scope: options.scope,\n ...(options.sessionId ? { sessionId: options.sessionId } : {}),\n ...(options.ttlMinutes ? { ttlMinutes: options.ttlMinutes } : {}),\n })\n return ok({ token: token.token, expiresAt: token.expiresAt, scope: token.scope })\n } catch (err) {\n return fail(err)\n }\n}"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/sandbox/index.ts"],"sourcesContent":["import {\n Sandbox,\n type AgentProfile,\n type AgentProfileFileMount,\n type AgentProfileMcpServer,\n type SandboxInstance,\n type ScopedTokenScope,\n type StorageConfig,\n type PromptResult,\n} from '@tangle-network/sandbox'\nimport {\n buildAppToolMcpServer,\n type AppToolName,\n type AppToolContext,\n type ToolHeaderNames,\n} from '../tools/index'\nimport { assertHarnessModelCompatible, type Harness } from '../harness/index'\n\nexport type Outcome<T> =\n | { succeeded: true; value: T }\n | { succeeded: false; error: Error }\n\nconst ok = <T>(value: T): Outcome<T> => ({ succeeded: true, value })\nconst fail = (error: unknown): Outcome<never> => ({\n succeeded: false,\n error: error instanceof Error ? error : new Error(String(error)),\n})\n\nexport interface SandboxClientCredentials {\n apiKey: string\n baseUrl: string\n}\n\nexport interface SandboxResourceConfig {\n image: string\n cpuCores: number\n memoryMB: number\n diskGB: number\n maxLifetimeSeconds: number\n idleTimeoutSeconds: number\n}\n\nexport interface ProviderResolutionConfig {\n routerBaseUrl?: string\n apiKey?: string\n providerName?: string\n modelName?: string\n defaultModel?: string\n openaiApiKey?: string\n}\n\nexport interface SandboxBuildContext {\n workspaceId: string\n connectedIntegrationIds: string[]\n userId?: string\n}\n\n// SDK-typed snapshot storage config (re-exported for product seam closures).\nexport type { StorageConfig }\n\n// Scope handed to per-identity seams. workspaceId is always present; userId is\n// present when the lifecycle op carries one. Workspace-keyed products ignore userId.\nexport interface SandboxScope {\n workspaceId: string\n userId?: string\n}\n\n// Snapshot RESTORE-on-create. Returned alongside storage; undefined => fresh box.\nexport interface SandboxRestoreSpec {\n fromSnapshot: string\n fromSandboxId: string\n}\n\n// Reuse health gate + sidecar liveness. The exec+timeout-race is generic; the\n// sidecarProcessPattern is harness-specific (which process is the live sidecar),\n// so it is a closure. Absent => no liveness probe (reuse on metadata.harness match).\nexport interface LivenessProbeConfig {\n sidecarProcessPattern: (harness: Harness) => string\n execTimeoutMs?: number\n psTimeoutMs?: number\n}\n\nexport interface ProfileComposeOptions {\n systemPrompt?: string\n extraFiles?: AgentProfileFileMount[]\n extraMcp?: Record<string, AgentProfileMcpServer>\n name?: string\n}\n\nexport interface SandboxRuntimeConfig {\n // Widened to accept an optional scope and be async so a per-user key can be\n // minted. The sync, no-arg form still satisfies the type (back-compat).\n credentials: (\n scope?: SandboxScope,\n ) => SandboxClientCredentials | null | Promise<SandboxClientCredentials | null>\n name: (workspaceId: string) => string\n metadata: (harness: Harness) => Record<string, unknown>\n connectedIntegrationIds: (workspaceId: string) => Promise<string[]>\n env: (ctx: SandboxBuildContext) => Promise<Record<string, string>>\n files: (ctx: SandboxBuildContext) => Promise<AgentProfileFileMount[]>\n secrets: (workspaceId: string) => Promise<string[]>\n profile: (options: ProfileComposeOptions) => AgentProfile\n permissionRole?: (workspaceRole: string) => SandboxPermissionLevel\n resources?: SandboxResourceConfig\n provider?: ProviderResolutionConfig\n\n // BYOS3/R2 snapshot storage. Returns undefined => key omitted entirely\n // (fail-closed when creds absent). Product owns bucket/endpoint/credentials/prefix.\n storage?: (ctx: SandboxBuildContext) => StorageConfig | undefined\n // Snapshot RESTORE-on-create. undefined => fresh box.\n restore?: (ctx: SandboxBuildContext) => SandboxRestoreSpec | undefined\n // Per-identity box NAME. Defaults to name(scope.workspaceId) when absent.\n boxKey?: (scope: SandboxScope) => string\n // Per-workspace child-key mint: overrides the resolved model apiKey before create.\n // Applied only when a model is resolved and its provider is openai-compat.\n childKeyMint?: (scope: SandboxScope) => Promise<Outcome<string>>\n // One-shot post-running bootstrap, on BOTH create and reuse paths (idempotency\n // is the closure's job — it owns the marker check).\n bootstrap?: (box: SandboxInstance, scope: SandboxScope) => Promise<Outcome<void>>\n // Reuse health gate + sidecar liveness probe.\n livenessProbe?: LivenessProbeConfig\n // default true: try stopped-resume before create.\n resumeStopped?: boolean\n // default false: bake resolveModel() into backend.model at create.\n backendModelAtCreate?: boolean\n}\n\nexport const DEFAULT_SANDBOX_RESOURCES: SandboxResourceConfig = {\n image: 'universal',\n cpuCores: 2,\n memoryMB: 4096,\n diskGB: 10,\n maxLifetimeSeconds: 86400,\n idleTimeoutSeconds: 3600,\n}\n\ninterface ClientCacheEntry {\n client: Sandbox\n fingerprint: string\n}\n\nlet _cached: ClientCacheEntry | null = null\n\nfunction getClientFromCreds(creds: SandboxClientCredentials): Sandbox {\n const fingerprint = `${creds.apiKey} ${creds.baseUrl}`\n if (_cached && _cached.fingerprint === fingerprint) return _cached.client\n\n const client = new Sandbox({ apiKey: creds.apiKey, baseUrl: creds.baseUrl })\n _cached = { client, fingerprint }\n return client\n}\n\n// Sync client for non-scoped callers (secretStoreFromClient etc.). Resolves\n// credentials with no scope; throws if the seam returns a Promise — scoped\n// products must use the async ensureWorkspaceSandbox path.\nexport function getClient(shell: SandboxRuntimeConfig): Sandbox {\n const creds = shell.credentials()\n if (creds && typeof (creds as Promise<unknown>).then === 'function') {\n throw new Error('getClient: scoped (async) credentials require the async sandbox path')\n }\n if (!creds) throw new Error('sandbox credentials are required (apiKey/baseUrl)')\n return getClientFromCreds(creds as SandboxClientCredentials)\n}\n\nexport function resetClientCache(): void {\n _cached = null\n}\n\nexport interface AppToolDescriptor {\n tool: AppToolName\n key: string\n description: string\n}\n\nexport interface BuildAppToolMcpServersOptions {\n tools: AppToolDescriptor[]\n baseUrl: string\n token: string\n ctx: AppToolContext\n headerNames?: ToolHeaderNames\n}\n\nexport function buildAppToolMcpServers(\n options: BuildAppToolMcpServersOptions,\n): Record<string, AgentProfileMcpServer> {\n const entries: Record<string, AgentProfileMcpServer> = {}\n for (const { tool, key, description } of options.tools) {\n entries[key] = buildAppToolMcpServer({\n tool,\n baseUrl: options.baseUrl,\n token: options.token,\n ctx: options.ctx,\n description,\n headerNames: options.headerNames,\n }) as AgentProfileMcpServer\n }\n return entries\n}\n\nexport interface EnsureWorkspaceSandboxOptions {\n workspaceId: string\n userId?: string\n harness: Harness\n // When set, both the running-reuse and stopped-resume short-circuits are\n // skipped and any name-matched box is deleted before create.\n forceNew?: boolean\n}\n\n// Single-quote a string for safe interpolation into a shell command.\nfunction shellSingleQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\nasync function listRunning(\n client: Sandbox,\n name: string,\n): Promise<Outcome<SandboxInstance | null>> {\n try {\n const running = await client.list({ status: 'running' })\n return ok(running.find((s) => s.name === name) ?? null)\n } catch (err) {\n return fail(err)\n }\n}\n\nasync function deleteBox(box: SandboxInstance): Promise<Outcome<void>> {\n try {\n await box.delete()\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\n// The SDK narrows `backend.type` to its own BackendType union and\n// `initialUsers[].role` to PermissionLevel — neither symbol is exported. The\n// create payload is assembled with the product's Harness/role strings, which\n// are a superset surface; the localized cast at the boundary is the only place\n// this widening is allowed, and the runtime contract (the sidecar boots the\n// named harness) is what enforces correctness.\ntype CreatePayload = Parameters<Sandbox['create']>[0]\n\n// Generic exec+sidecar liveness probe. Absent probe => always alive (the prior\n// reuse-on-metadata-match behavior). With a probe: the container must answer an\n// `echo alive` exec within execTimeoutMs, and the sidecar process must be found\n// by pgrep within psTimeoutMs (an inconclusive pgrep is treated as reusable).\nasync function isBoxAlive(\n box: SandboxInstance,\n harness: Harness,\n probe: LivenessProbeConfig | undefined,\n): Promise<boolean> {\n if (!probe) return true\n const execTimeout = probe.execTimeoutMs ?? 5000\n const psTimeout = probe.psTimeoutMs ?? 3000\n const race = <T>(p: Promise<T>, ms: number, label: string): Promise<T> =>\n Promise.race([\n p,\n new Promise<T>((_, reject) => setTimeout(() => reject(new Error(label)), ms)),\n ])\n try {\n const alive = await race(box.exec('echo alive'), execTimeout, 'alive check timeout')\n if (!alive.stdout.includes('alive')) return false\n const pattern = probe.sidecarProcessPattern(harness)\n try {\n const ps = await race(\n box.exec(`pgrep -f ${shellSingleQuote(pattern)} || echo no-sidecar`),\n psTimeout,\n 'ps check timeout',\n )\n if (ps.stdout.includes('no-sidecar')) return false\n } catch {\n // sidecar probe inconclusive — container is alive, treat as reusable\n }\n return true\n } catch {\n return false\n }\n}\n\n// Resume a name-matched stopped box and wait for it to reach running. Returns\n// ok(null) when no stopped box matches the name.\nasync function resumeStoppedBox(\n client: Sandbox,\n name: string,\n timeoutMs: number,\n): Promise<Outcome<SandboxInstance | null>> {\n try {\n const stopped = await client.list({ status: 'stopped' })\n const match = stopped.find((s) => s.name === name) ?? null\n if (!match) return ok(null)\n await match.resume()\n await match.waitFor('running', { timeoutMs })\n return ok(match)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function ensureWorkspaceSandbox(\n shell: SandboxRuntimeConfig,\n options: EnsureWorkspaceSandboxOptions,\n): Promise<SandboxInstance> {\n const { workspaceId, userId, harness, forceNew } = options\n const scope: SandboxScope = { workspaceId, ...(userId ? { userId } : {}) }\n const creds = await shell.credentials(scope)\n if (!creds) throw new Error('sandbox credentials are required (apiKey/baseUrl)')\n const client = getClientFromCreds(creds)\n const name = shell.boxKey ? shell.boxKey(scope) : shell.name(workspaceId)\n const resources = shell.resources ?? DEFAULT_SANDBOX_RESOURCES\n const resumeTimeout = 120_000\n\n // Stage 1 — running-box reuse (skipped on forceNew).\n const existing = await listRunning(client, name)\n if (existing.succeeded && existing.value) {\n const found = existing.value\n if (forceNew) {\n const dropped = await deleteBox(found)\n if (!dropped.succeeded) {\n throw new Error(`forceNew: sandbox ${name} could not be deleted`, { cause: dropped.error })\n }\n } else if (\n found.metadata?.harness === harness &&\n (await isBoxAlive(found, harness, shell.livenessProbe))\n ) {\n if (shell.bootstrap) {\n const boot = await shell.bootstrap(found, scope)\n if (!boot.succeeded) {\n throw new Error(`bootstrap failed on reused box ${name}`, { cause: boot.error })\n }\n }\n return found\n } else {\n const dropped = await deleteBox(found)\n if (!dropped.succeeded) {\n throw new Error(\n `sandbox ${name} ` +\n `(was ${String(found.metadata?.harness ?? 'unknown')}, want ${harness}, or unresponsive) ` +\n `could not be deleted`,\n { cause: dropped.error },\n )\n }\n }\n }\n\n // Stage 2 — stopped-box resume (skipped on forceNew or resumeStopped===false).\n if (!forceNew && shell.resumeStopped !== false) {\n const resumed = await resumeStoppedBox(client, name, resumeTimeout)\n if (\n resumed.succeeded &&\n resumed.value &&\n (await isBoxAlive(resumed.value, harness, shell.livenessProbe))\n ) {\n const box = resumed.value\n if (shell.bootstrap) {\n const boot = await shell.bootstrap(box, scope)\n if (!boot.succeeded) {\n throw new Error(`bootstrap failed on resumed box ${name}`, { cause: boot.error })\n }\n }\n return box\n }\n }\n\n // Stage 3 — create fresh.\n const connectedIntegrationIds = await shell.connectedIntegrationIds(workspaceId)\n const buildCtx: SandboxBuildContext = {\n workspaceId,\n connectedIntegrationIds,\n ...(userId ? { userId } : {}),\n }\n const [secrets, env, files] = await Promise.all([\n shell.secrets(workspaceId),\n shell.env(buildCtx),\n shell.files(buildCtx),\n ])\n const profile = shell.profile({ extraFiles: files })\n\n const role = userId && shell.permissionRole ? shell.permissionRole('developer') : undefined\n\n // Bake the model at create when opted in. childKeyMint overrides the apiKey\n // per-workspace; a typed mint failure falls through to the parent key (logged).\n let model = shell.backendModelAtCreate ? resolveModel(shell.provider) : undefined\n if (model && shell.childKeyMint && model.provider === 'openai-compat') {\n const minted = await shell.childKeyMint(scope)\n if (minted.succeeded) model = { ...model, apiKey: minted.value }\n else {\n console.error(\n `[sandbox] childKeyMint failed for ${workspaceId}; using parent key:`,\n minted.error.message,\n )\n }\n }\n\n const storage = shell.storage?.(buildCtx)\n const restore = shell.restore?.(buildCtx)\n\n const payload = {\n name,\n image: resources.image,\n metadata: shell.metadata(harness),\n ...(userId ? { permissions: { initialUsers: [{ userId, role }] } } : {}),\n env,\n secrets,\n backend: { type: harness, profile, ...(model ? { model } : {}) },\n ...(storage ? { storage } : {}),\n ...(restore ? restore : {}),\n maxLifetimeSeconds: resources.maxLifetimeSeconds,\n idleTimeoutSeconds: resources.idleTimeoutSeconds,\n resources: {\n cpuCores: resources.cpuCores,\n memoryMB: resources.memoryMB,\n diskGB: resources.diskGB,\n },\n } as CreatePayload\n\n const box = await client.create(payload)\n\n await box.waitFor('running', { timeoutMs: 120_000 })\n if (!box.connection?.runtimeUrl) await box.refresh()\n\n if (shell.bootstrap) {\n const boot = await shell.bootstrap(box, scope)\n if (!boot.succeeded) {\n throw new Error(`bootstrap failed on new box ${name}`, { cause: boot.error })\n }\n }\n return box\n}\n\nexport interface ResolvedModel {\n model: string\n provider: string\n apiKey: string\n baseUrl?: string\n}\n\nexport function resolveModel(\n config: ProviderResolutionConfig | undefined,\n override?: { model?: string; modelApiKey?: string },\n): ResolvedModel | undefined {\n const c = config ?? {}\n const explicitBaseUrl = c.routerBaseUrl\n const explicitApiKey = override?.modelApiKey ?? c.apiKey\n const provider =\n c.providerName ?? (explicitApiKey ? 'openai-compat' : c.openaiApiKey ? 'openai' : undefined)\n const modelName =\n override?.model ??\n c.modelName ??\n (provider === 'openai' || provider === 'openai-compat' ? c.defaultModel : undefined)\n const apiKey = explicitApiKey ?? (provider === 'openai' ? c.openaiApiKey : undefined)\n if (!provider || !modelName || !apiKey) return undefined\n return {\n model: modelName,\n provider,\n apiKey,\n ...(explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}),\n }\n}\n\nexport function flattenHistory(\n message: string,\n history?: Array<{ role: 'user' | 'assistant'; content: string }>,\n): string {\n if (!history?.length) return message\n const transcript = history\n .map((entry) => `${entry.role === 'assistant' ? 'Assistant' : 'User'}: ${entry.content}`)\n .join('\\n\\n')\n return `${transcript}\\n\\nUser: ${message}`\n}\n\nexport function mergeExtraMcp(\n appToolMcp: Record<string, AgentProfileMcpServer>,\n baseProfileMcp: Record<string, AgentProfileMcpServer>,\n extra: Record<string, AgentProfileMcpServer> | undefined,\n): Record<string, AgentProfileMcpServer> {\n for (const key of Object.keys(extra ?? {})) {\n if (key in appToolMcp || key in baseProfileMcp) {\n throw new Error(`extraMcp key '${key}' collides with an existing profile MCP server`)\n }\n }\n return { ...appToolMcp, ...(extra ?? {}) }\n}\n\nexport function attachReasoningEffort(\n profile: AgentProfile,\n harness: Harness,\n effort: 'auto' | 'low' | 'medium' | 'high' | undefined,\n): AgentProfile {\n if (!effort || effort === 'auto') return profile\n return {\n ...profile,\n extensions: {\n ...(profile.extensions ?? {}),\n [harness]: {\n ...(profile.extensions?.[harness] ?? {}),\n reasoningEffort: effort,\n },\n },\n }\n}\n\nexport interface StreamSandboxPromptOptions {\n sessionId?: string\n executionId?: string\n lastEventId?: string\n systemPrompt?: string\n model?: string\n modelApiKey?: string\n history?: Array<{ role: 'user' | 'assistant'; content: string }>\n harness?: Harness\n effort?: 'auto' | 'low' | 'medium' | 'high'\n appToolMcp?: Record<string, AgentProfileMcpServer>\n baseProfileMcp?: Record<string, AgentProfileMcpServer>\n extraMcp?: Record<string, AgentProfileMcpServer>\n signal?: AbortSignal\n timeoutMs?: number\n // When true, an interactive question event throws instead of yielding —\n // detached (cron/mission-step) runs have no consumer to answer it.\n disallowQuestions?: boolean\n}\n\ntype StreamPromptOptions = Parameters<SandboxInstance['streamPrompt']>[1]\n\nexport async function* streamSandboxPrompt(\n shell: SandboxRuntimeConfig,\n box: SandboxInstance,\n message: string,\n options?: StreamSandboxPromptOptions,\n): AsyncGenerator<unknown> {\n const harness = options?.harness ?? 'opencode'\n const model = resolveModel(shell.provider, {\n model: options?.model,\n modelApiKey: options?.modelApiKey,\n })\n\n // Server-side enforcement of the harness↔model policy: a vendor-locked harness\n // (claude-code/codex/kimi-code) must not be sent a foreign-provider model, even\n // if the UI snap was bypassed. Provider-less ids pass (session's own config).\n if (model?.model) assertHarnessModelCompatible(harness, model.model)\n\n const prompt = flattenHistory(message, options?.history)\n\n const appToolMcp = options?.appToolMcp ?? {}\n const extraMcp = mergeExtraMcp(appToolMcp, options?.baseProfileMcp ?? {}, options?.extraMcp)\n\n const profile = shell.profile({ systemPrompt: options?.systemPrompt, extraMcp })\n const profileWithEffort = attachReasoningEffort(profile, harness, options?.effort)\n\n const stream = box.streamPrompt(prompt, {\n sessionId: options?.sessionId,\n executionId: options?.executionId,\n lastEventId: options?.lastEventId,\n ...(options?.signal ? { signal: options.signal } : {}),\n ...(options?.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n backend: {\n type: harness,\n profile: profileWithEffort,\n ...(model ? { model } : {}),\n },\n } as StreamPromptOptions)\n\n let severedFinishReason: string | null = null\n for await (const event of stream) {\n const step = classifySeveredStream(event)\n if (step) severedFinishReason = step.kind === 'step-finish' && step.severed ? step.reason : null\n if (severedFinishReason && isTerminalPromptEvent(event)) {\n throw new Error(`sandbox model stream severed mid-turn (reason=\"${severedFinishReason}\")`)\n }\n if (options?.disallowQuestions) {\n const q = detectInteractiveQuestion(event)\n if (q) {\n throw new Error(`sandbox agent asked an interactive question during an autonomous run: ${q}`)\n }\n }\n yield event\n }\n // Reconnect-exhausted path: the stream ended on a severed step without a\n // terminal event. A truncated turn must fail loud, not return silently.\n if (severedFinishReason) {\n throw new Error(`sandbox model stream severed mid-turn (reason=\"${severedFinishReason}\")`)\n }\n}\n\nexport async function runSandboxPrompt(\n shell: SandboxRuntimeConfig,\n box: SandboxInstance,\n message: string,\n options?: StreamSandboxPromptOptions,\n): Promise<string> {\n let fullText = ''\n let firstTextSeen = false\n\n for await (const rawEvent of streamSandboxPrompt(shell, box, message, options)) {\n const event = rawEvent as { type?: string; data?: Record<string, unknown> }\n if (!event.type) continue\n\n if (event.type === 'message.part.updated') {\n const part = event.data?.part as Record<string, unknown> | undefined\n const delta = typeof event.data?.delta === 'string' ? event.data.delta : null\n if (String(part?.type ?? '') === 'text') {\n if (!firstTextSeen) {\n firstTextSeen = true\n continue\n }\n if (delta) fullText += delta\n else if (typeof part?.text === 'string') fullText = part.text\n }\n } else if (event.type === 'result') {\n const finalText = typeof event.data?.finalText === 'string' ? event.data.finalText : null\n if (finalText) fullText = finalText\n }\n }\n\n return fullText\n}\n\n// Mirrors the SDK's PermissionLevel union (not re-exported by\n// @tangle-network/sandbox). The product's role-mapping seam must produce one of\n// these; binding the seam's return type to the union makes a wrong mapping a\n// compile error rather than a runtime 400 from the orchestrator.\nexport type SandboxPermissionLevel = 'owner' | 'admin' | 'developer' | 'viewer'\n\nexport interface MemberSyncSeam {\n roleToSandboxRole: (workspaceRole: string) => SandboxPermissionLevel\n}\n\nexport async function syncSandboxMemberAdd(\n box: SandboxInstance,\n seam: MemberSyncSeam,\n userId: string,\n role: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.add({ userId, role: seam.roleToSandboxRole(role) })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function syncSandboxMemberRemove(\n box: SandboxInstance,\n userId: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.remove(userId, { preserveHomeDir: true })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function syncSandboxMemberRole(\n box: SandboxInstance,\n seam: MemberSyncSeam,\n userId: string,\n role: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.update(userId, { role: seam.roleToSandboxRole(role) })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport interface SecretStore {\n create: (name: string, value: string) => Promise<void>\n update: (name: string, value: string) => Promise<void>\n get: (name: string) => Promise<string>\n delete: (name: string) => Promise<void>\n}\n\nexport function secretStoreFromClient(shell: SandboxRuntimeConfig): SecretStore {\n const client = getClient(shell)\n return {\n create: async (name, value) => {\n await client.secrets.create(name, value)\n },\n update: async (name, value) => {\n await client.secrets.update(name, value)\n },\n get: (name) => client.secrets.get(name),\n delete: async (name) => {\n await client.secrets.delete(name)\n },\n }\n}\n\nexport async function storeSecret(\n store: SecretStore,\n name: string,\n value: string,\n): Promise<Outcome<void>> {\n try {\n await store.create(name, value)\n return ok(undefined)\n } catch {\n try {\n await store.update(name, value)\n return ok(undefined)\n } catch (err) {\n return fail(new Error(`Failed to store sandbox secret ${name}`, { cause: err }))\n }\n }\n}\n\nexport async function readSecret(store: SecretStore, name: string): Promise<Outcome<string>> {\n try {\n return ok(await store.get(name))\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function deleteSecret(store: SecretStore, name: string): Promise<Outcome<void>> {\n try {\n await store.delete(name)\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport interface ScopedTokenResult {\n token: string\n expiresAt: Date\n scope: ScopedTokenScope\n}\n\n/**\n * Mint a scoped token for an already-provisioned box (e.g. to hand a terminal\n * proxy a narrowed credential). Uses the SDK's native `box.mintScopedToken`,\n * which normalizes `expiresAt` to a Date — no hand-rolled wire call.\n */\nexport async function mintSandboxScopedToken(\n box: SandboxInstance,\n options: { scope: ScopedTokenScope; sessionId?: string; ttlMinutes?: number },\n): Promise<Outcome<ScopedTokenResult>> {\n try {\n const token = await box.mintScopedToken({\n scope: options.scope,\n ...(options.sessionId ? { sessionId: options.sessionId } : {}),\n ...(options.ttlMinutes ? { ttlMinutes: options.ttlMinutes } : {}),\n })\n return ok({ token: token.token, expiresAt: token.expiresAt, scope: token.scope })\n } catch (err) {\n return fail(err)\n }\n}\n\n// Detached single-turn advance. The SDK SandboxInstance has no `driveTurn`; the\n// non-streaming sibling of streamPrompt is box.prompt(message, opts) -> PromptResult.\n// Returns a typed Outcome so a failed turn is inspected, not swallowed.\nexport async function driveSandboxTurn(\n shell: SandboxRuntimeConfig,\n box: SandboxInstance,\n message: string,\n options: StreamSandboxPromptOptions & { sessionId: string },\n): Promise<Outcome<PromptResult>> {\n const harness = options.harness ?? 'opencode'\n const model = resolveModel(shell.provider, {\n model: options.model,\n modelApiKey: options.modelApiKey,\n })\n if (model?.model) assertHarnessModelCompatible(harness, model.model)\n const prompt = flattenHistory(message, options.history)\n const appToolMcp = options.appToolMcp ?? {}\n const extraMcp = mergeExtraMcp(appToolMcp, options.baseProfileMcp ?? {}, options.extraMcp)\n const profile = attachReasoningEffort(\n shell.profile({ systemPrompt: options.systemPrompt, extraMcp }),\n harness,\n options.effort,\n )\n try {\n const result = await box.prompt(prompt, {\n sessionId: options.sessionId,\n ...(options.executionId ? { executionId: options.executionId } : {}),\n ...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n backend: { type: harness, profile, ...(model ? { model } : {}) },\n } as Parameters<SandboxInstance['prompt']>[1])\n if (!result.success) return fail(new Error(result.error ?? 'sandbox turn failed'))\n return ok(result)\n } catch (err) {\n return fail(err)\n }\n}\n\n// Terminal-proxy HMAC token. Identity tuple is generic; the secret comes from a\n// closure (fail-loud if absent).\nexport interface TerminalProxyIdentity {\n userId: string\n workspaceId: string\n sandboxId: string\n}\n\nconst TERMINAL_PROXY_TOKEN_TTL_MS = 15 * 60 * 1000\n\nasync function signTerminalProxyToken(secret: string, encodedPayload: string): Promise<string> {\n const key = await crypto.subtle.importKey(\n 'raw',\n new TextEncoder().encode(secret),\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign'],\n )\n const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(encodedPayload))\n return base64UrlEncodeBytes(new Uint8Array(sig))\n}\n\nexport async function mintTerminalProxyToken(\n secret: string,\n identity: TerminalProxyIdentity,\n ttlMs = TERMINAL_PROXY_TOKEN_TTL_MS,\n): Promise<Outcome<{ token: string; expiresAt: Date }>> {\n if (!secret) return fail(new Error('mintTerminalProxyToken: secret is required'))\n if (!identity.userId || !identity.workspaceId || !identity.sandboxId) {\n return fail(new Error('mintTerminalProxyToken: userId/workspaceId/sandboxId are required'))\n }\n const expiresAt = new Date(Date.now() + ttlMs)\n const payload = { ...identity, exp: Math.floor(expiresAt.getTime() / 1000) }\n const encoded = base64UrlEncodeUtf8(JSON.stringify(payload))\n const sig = await signTerminalProxyToken(secret, encoded)\n return ok({ token: `${encoded}.${sig}`, expiresAt })\n}\n\nexport async function verifyTerminalProxyToken(\n secret: string,\n token: string,\n expected: TerminalProxyIdentity,\n): Promise<boolean> {\n if (!secret) return false\n const [encoded, sig, extra] = token.split('.')\n if (!encoded || !sig || extra !== undefined) return false\n const expectedSig = await signTerminalProxyToken(secret, encoded)\n if (!constantTimeEqual(sig, expectedSig)) return false\n let payload: TerminalProxyIdentity & { exp: number }\n try {\n payload = JSON.parse(base64UrlDecodeUtf8(encoded))\n } catch {\n return false\n }\n return (\n payload.userId === expected.userId &&\n payload.workspaceId === expected.workspaceId &&\n payload.sandboxId === expected.sandboxId &&\n Number.isFinite(payload.exp) &&\n payload.exp > Math.floor(Date.now() / 1000)\n )\n}\n\nfunction base64UrlEncodeBytes(bytes: Uint8Array): string {\n let bin = ''\n for (const b of bytes) bin += String.fromCharCode(b)\n return btoa(bin).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '')\n}\n\nfunction base64UrlEncodeUtf8(v: string): string {\n return base64UrlEncodeBytes(new TextEncoder().encode(v))\n}\n\nfunction base64UrlDecodeUtf8(v: string): string {\n const padded = v.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(v.length / 4) * 4, '=')\n const bin = atob(padded)\n const bytes = new Uint8Array(bin.length)\n for (let i = 0; i < bin.length; i += 1) bytes[i] = bin.charCodeAt(i)\n return new TextDecoder().decode(bytes)\n}\n\nfunction constantTimeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false\n let r = 0\n for (let i = 0; i < a.length; i += 1) r |= a.charCodeAt(i) ^ b.charCodeAt(i)\n return r === 0\n}\n\n// Severed-stream classifier. Generic to any router-backed harness: a final step\n// that finished with error/other/unknown is a truncated turn, not a completed one.\nconst SEVERED_FINISH_REASONS = new Set(['error', 'other', 'unknown'])\n\nexport type SandboxStepTransition =\n | { kind: 'step-start' }\n | { kind: 'step-finish'; reason: string; severed: boolean }\n\nfunction asPlainRecord(v: unknown): Record<string, unknown> | null {\n return v && typeof v === 'object' && !Array.isArray(v) ? (v as Record<string, unknown>) : null\n}\n\nexport function classifySeveredStream(event: unknown): SandboxStepTransition | null {\n const root = asPlainRecord(event)\n if (!root || root.type !== 'message.part.updated') return null\n const body = asPlainRecord(root.properties) ?? asPlainRecord(root.data) ?? root\n const part = asPlainRecord(body.part)\n if (!part) return null\n if (part.type === 'step-start') return { kind: 'step-start' }\n if (part.type !== 'step-finish') return null\n const reason = typeof part.reason === 'string' && part.reason ? part.reason : 'unknown'\n return { kind: 'step-finish', reason, severed: SEVERED_FINISH_REASONS.has(reason) }\n}\n\nexport function isTerminalPromptEvent(event: unknown): boolean {\n const t = asPlainRecord(event)?.type\n return t === 'result' || t === 'done'\n}\n\n// Interactive-question detector. Returns the question text or null. Used by\n// streamSandboxPrompt when disallowQuestions is set.\nexport function detectInteractiveQuestion(event: unknown): string | null {\n const root = asPlainRecord(event)\n if (!root) return null\n const type = typeof root.type === 'string' ? root.type : undefined\n const data = asPlainRecord(root.data)\n const props = asPlainRecord(root.properties)\n const body = props ?? data ?? root\n if (type === 'question.asked' || type === 'question') return firstQuestionText(body)\n const part = asPlainRecord(data?.part) ?? asPlainRecord(body.part)\n const tool =\n (typeof part?.tool === 'string' && part.tool) ||\n (typeof part?.name === 'string' && part.name) ||\n (typeof body.tool === 'string' && body.tool) ||\n undefined\n const isQ =\n type === 'message.part.updated' &&\n (tool === 'question' || asPlainRecord(part)?.type === 'question')\n if (!isQ) return null\n const state = asPlainRecord(asPlainRecord(part)?.state)\n return firstQuestionText(asPlainRecord(state?.input) ?? state ?? part ?? body)\n}\n\nfunction firstQuestionText(value: Record<string, unknown> | null): string {\n const arr = Array.isArray(value?.questions)\n ? value!.questions\n : Array.isArray(asPlainRecord(value?.input)?.questions)\n ? (asPlainRecord(value!.input)!.questions as unknown[])\n : []\n const first = asPlainRecord(arr[0])\n const q =\n (typeof first?.question === 'string' && first.question) ||\n (typeof first?.prompt === 'string' && first.prompt) ||\n undefined\n return q ?? 'interactive question'\n}"],"mappings":"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","names":["box"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/agent-app",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "packageManager": "pnpm@10.33.4",
5
5
  "description": "Application-shell framework for Tangle agent products: a bounded tool loop, the structured agent→app tool side channel, integration-hub client, per-workspace billing, and crypto — composed over the Tangle agent substrate through typed seams.",
6
6
  "keywords": [
@@ -1,43 +0,0 @@
1
- // src/harness/index.ts
2
- var KNOWN_HARNESSES = [
3
- "opencode",
4
- "claude-code",
5
- "kimi-code",
6
- "codex",
7
- "amp",
8
- "factory-droids",
9
- "pi",
10
- "hermes",
11
- "forge",
12
- "openclaw",
13
- "acp",
14
- "cursor",
15
- "cli-base"
16
- ];
17
- var DEFAULT_HARNESS = "opencode";
18
- var HARNESS_SET = new Set(KNOWN_HARNESSES);
19
- function isHarness(value) {
20
- return typeof value === "string" && HARNESS_SET.has(value);
21
- }
22
- function coerceHarness(value, fallback = DEFAULT_HARNESS) {
23
- return isHarness(value) ? value : fallback;
24
- }
25
- function resolveSessionHarness(input = {}) {
26
- const fallback = input.fallback ?? DEFAULT_HARNESS;
27
- if (isHarness(input.sessionHarness)) {
28
- const locked = input.sessionHarness;
29
- const swapAttempted = isHarness(input.requested) && input.requested !== locked;
30
- return { harness: locked, locked: true, swapAttempted };
31
- }
32
- const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback));
33
- return { harness, locked: false, swapAttempted: false };
34
- }
35
-
36
- export {
37
- KNOWN_HARNESSES,
38
- DEFAULT_HARNESS,
39
- isHarness,
40
- coerceHarness,
41
- resolveSessionHarness
42
- };
43
- //# sourceMappingURL=chunk-SD2H4FWY.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/harness/index.ts"],"sourcesContent":["/**\n * Coding-agent harness selection — taxonomy, coercion, and the session-lock invariant.\n *\n * A \"harness\" is the coding-agent CLI a sandbox drives (opencode / codex /\n * claude-code / …). The shell governs WHICH harness a chat session uses and\n * enforces that a session is LOCKED to the harness it started with — the model\n * may change mid-session, the harness may not (swapping it mid-session would\n * orphan the session's running agent state). Every product otherwise hand-rolls\n * this and hard-codes a single harness; this is the one place the rule lives.\n *\n * Substrate-free: the harness list mirrors the sandbox SDK's `BackendType` as a\n * plain string union (no sandbox dependency). The consumer owns storage — which\n * harness a workspace defaults to, which one a session locked — and maps the\n * resolved value onto the SDK's `backend.type`.\n */\n\n/** The known coding-agent backends. Mirrors `@tangle-network/sandbox`'s\n * `BackendType`; kept structural so this module needs no sandbox dependency. */\nexport const KNOWN_HARNESSES = [\n 'opencode',\n 'claude-code',\n 'kimi-code',\n 'codex',\n 'amp',\n 'factory-droids',\n 'pi',\n 'hermes',\n 'forge',\n 'openclaw',\n 'acp',\n 'cursor',\n 'cli-base',\n] as const\n\nexport type Harness = (typeof KNOWN_HARNESSES)[number]\n\nexport const DEFAULT_HARNESS: Harness = 'opencode'\n\nconst HARNESS_SET: ReadonlySet<string> = new Set(KNOWN_HARNESSES)\n\nexport function isHarness(value: unknown): value is Harness {\n return typeof value === 'string' && HARNESS_SET.has(value)\n}\n\n/** Coerce an arbitrary value to a known harness, falling back (default `opencode`). */\nexport function coerceHarness(value: unknown, fallback: Harness = DEFAULT_HARNESS): Harness {\n return isHarness(value) ? value : fallback\n}\n\nexport interface ResolveSessionHarnessInput {\n /** The harness already locked to this session (recorded at its first turn). */\n sessionHarness?: unknown\n /** The harness requested now — a new session's choice, or a turn's attempt to switch. */\n requested?: unknown\n /** The workspace's default harness, used only when starting a fresh session. */\n workspaceDefault?: unknown\n /** Final fallback when nothing else resolves (default `opencode`). */\n fallback?: Harness\n}\n\nexport interface ResolvedSessionHarness {\n /** The harness to actually run — the locked one when the session already has it. */\n harness: Harness\n /** True when the session already had a locked harness (this turn did not pick it). */\n locked: boolean\n /** True when `requested` differs from the locked harness — a forbidden mid-session\n * swap the caller should reject or warn on. The lock always wins regardless. */\n swapAttempted: boolean\n}\n\n/**\n * Resolve the harness for a turn, enforcing the session lock.\n *\n * - **Session already started** (`sessionHarness` is a known harness): that harness\n * wins (`locked: true`); a differing `requested` sets `swapAttempted` so the caller\n * can reject the swap. The model is a separate per-turn concern and is unaffected.\n * - **Fresh session**: pick `requested → workspaceDefault → fallback`. The caller\n * persists the result as the session's lock for every subsequent turn.\n */\nexport function resolveSessionHarness(input: ResolveSessionHarnessInput = {}): ResolvedSessionHarness {\n const fallback = input.fallback ?? DEFAULT_HARNESS\n if (isHarness(input.sessionHarness)) {\n const locked = input.sessionHarness\n const swapAttempted = isHarness(input.requested) && input.requested !== locked\n return { harness: locked, locked: true, swapAttempted }\n }\n const harness = coerceHarness(input.requested, coerceHarness(input.workspaceDefault, fallback))\n return { harness, locked: false, swapAttempted: false }\n}\n"],"mappings":";AAkBO,IAAM,kBAAkB;AAAA,EAC7B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,IAAM,kBAA2B;AAExC,IAAM,cAAmC,IAAI,IAAI,eAAe;AAEzD,SAAS,UAAU,OAAkC;AAC1D,SAAO,OAAO,UAAU,YAAY,YAAY,IAAI,KAAK;AAC3D;AAGO,SAAS,cAAc,OAAgB,WAAoB,iBAA0B;AAC1F,SAAO,UAAU,KAAK,IAAI,QAAQ;AACpC;AAgCO,SAAS,sBAAsB,QAAoC,CAAC,GAA2B;AACpG,QAAM,WAAW,MAAM,YAAY;AACnC,MAAI,UAAU,MAAM,cAAc,GAAG;AACnC,UAAM,SAAS,MAAM;AACrB,UAAM,gBAAgB,UAAU,MAAM,SAAS,KAAK,MAAM,cAAc;AACxE,WAAO,EAAE,SAAS,QAAQ,QAAQ,MAAM,cAAc;AAAA,EACxD;AACA,QAAM,UAAU,cAAc,MAAM,WAAW,cAAc,MAAM,kBAAkB,QAAQ,CAAC;AAC9F,SAAO,EAAE,SAAS,QAAQ,OAAO,eAAe,MAAM;AACxD;","names":[]}