@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.
- package/dist/chunk-5VXPDXZJ.js +101 -0
- package/dist/chunk-5VXPDXZJ.js.map +1 -0
- package/dist/harness/index.d.ts +37 -1
- package/dist/harness/index.js +17 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +16 -2
- package/dist/run/index.js +1 -1
- package/dist/sandbox/index.d.ts +54 -3
- package/dist/sandbox/index.js +276 -17
- package/dist/sandbox/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-SD2H4FWY.js +0 -43
- package/dist/chunk-SD2H4FWY.js.map +0 -1
|
@@ -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":[]}
|
package/dist/harness/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/harness/index.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
package/dist/sandbox/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { AgentProfileFileMount, AgentProfileMcpServer, AgentProfile,
|
|
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 };
|
package/dist/sandbox/index.js
CHANGED
|
@@ -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
|
|
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
|
|
70
|
-
const
|
|
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 (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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 = {
|
|
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
|
-
|
|
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":";;;;;;;AAAA;AAAA,EACE;AAAA,OAMK;AAaP,IAAM,KAAK,CAAI,WAA0B,EAAE,WAAW,MAAM,MAAM;AAClE,IAAM,OAAO,CAAC,WAAoC;AAAA,EAChD,WAAW;AAAA,EACX,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AACjE;AAmDO,IAAM,4BAAmD;AAAA,EAC9D,OAAO;AAAA,EACP,UAAU;AAAA,EACV,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,oBAAoB;AAAA,EACpB,oBAAoB;AACtB;AAOA,IAAI,UAAmC;AAEhC,SAAS,UAAU,OAAsC;AAC9D,QAAM,QAAQ,MAAM,YAAY;AAChC,MAAI,CAAC,MAAO,OAAM,IAAI,MAAM,mDAAmD;AAE/E,QAAM,cAAc,GAAG,MAAM,MAAM,IAAI,MAAM,OAAO;AACpD,MAAI,WAAW,QAAQ,gBAAgB,YAAa,QAAO,QAAQ;AAEnE,QAAM,SAAS,IAAI,QAAQ,EAAE,QAAQ,MAAM,QAAQ,SAAS,MAAM,QAAQ,CAAC;AAC3E,YAAU,EAAE,QAAQ,YAAY;AAChC,SAAO;AACT;AAEO,SAAS,mBAAyB;AACvC,YAAU;AACZ;AAgBO,SAAS,uBACd,SACuC;AACvC,QAAM,UAAiD,CAAC;AACxD,aAAW,EAAE,MAAM,KAAK,YAAY,KAAK,QAAQ,OAAO;AACtD,YAAQ,GAAG,IAAI,sBAAsB;AAAA,MACnC;AAAA,MACA,SAAS,QAAQ;AAAA,MACjB,OAAO,QAAQ;AAAA,MACf,KAAK,QAAQ;AAAA,MACb;AAAA,MACA,aAAa,QAAQ;AAAA,IACvB,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAQA,eAAe,YACb,QACA,MAC0C;AAC1C,MAAI;AACF,UAAM,UAAU,MAAM,OAAO,KAAK,EAAE,QAAQ,UAAU,CAAC;AACvD,WAAO,GAAG,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,IAAI,KAAK,IAAI;AAAA,EACxD,SAAS,KAAK;AACZ,WAAO,KAAK,GAAG;AAAA,EACjB;AACF;AAEA,eAAe,UAAU,KAA8C;AACrE,MAAI;AACF,UAAM,IAAI,OAAO;AACjB,WAAO,GAAG,MAAS;AAAA,EACrB,SAAS,KAAK;AACZ,WAAO,KAAK,GAAG;AAAA,EACjB;AACF;AAUA,eAAsB,uBACpB,OACA,SAC0B;AAC1B,QAAM,EAAE,aAAa,QAAQ,QAAQ,IAAI;AACzC,QAAM,SAAS,UAAU,KAAK;AAC9B,QAAM,OAAO,MAAM,KAAK,WAAW;AACnC,QAAM,YAAY,MAAM,aAAa;AAErC,QAAM,WAAW,MAAM,YAAY,QAAQ,IAAI;AAC/C,MAAI,SAAS,aAAa,SAAS,OAAO;AACxC,UAAM,QAAQ,SAAS;AACvB,QAAI,MAAM,UAAU,YAAY,QAAS,QAAO;AAChD,UAAM,UAAU,MAAM,UAAU,KAAK;AACrC,QAAI,CAAC,QAAQ,WAAW;AACtB,YAAM,IAAI;AAAA,QACR,8BAA8B,IAAI,SACxB,OAAO,MAAM,UAAU,WAAW,SAAS,CAAC,UAAU,OAAO;AAAA,QACvE,EAAE,OAAO,QAAQ,MAAM;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,0BAA0B,MAAM,MAAM,wBAAwB,WAAW;AAC/E,QAAM,WAAgC,EAAE,aAAa,wBAAwB;AAC7E,QAAM,CAAC,SAAS,KAAK,KAAK,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC9C,MAAM,QAAQ,WAAW;AAAA,IACzB,MAAM,IAAI,QAAQ;AAAA,IAClB,MAAM,MAAM,QAAQ;AAAA,EACtB,CAAC;AACD,QAAM,UAAU,MAAM,QAAQ,EAAE,YAAY,MAAM,CAAC;AAEnD,QAAM,OAAO,UAAU,MAAM,iBAAiB,MAAM,eAAe,WAAW,IAAI;AAElF,QAAM,UAAU;AAAA,IACd;AAAA,IACA,OAAO,UAAU;AAAA,IACjB,UAAU,MAAM,SAAS,OAAO;AAAA,IAChC,GAAI,SAAS,EAAE,aAAa,EAAE,cAAc,CAAC,EAAE,QAAQ,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC;AAAA,IACtE;AAAA,IACA;AAAA,IACA,SAAS,EAAE,MAAM,SAAS,QAAQ;AAAA,IAClC,oBAAoB,UAAU;AAAA,IAC9B,oBAAoB,UAAU;AAAA,IAC9B,WAAW;AAAA,MACT,UAAU,UAAU;AAAA,MACpB,UAAU,UAAU;AAAA,MACpB,QAAQ,UAAU;AAAA,IACpB;AAAA,EACF;AAEA,QAAM,MAAM,MAAM,OAAO,OAAO,OAAO;AAEvC,QAAM,IAAI,QAAQ,WAAW,EAAE,WAAW,KAAQ,CAAC;AACnD,MAAI,CAAC,IAAI,YAAY,WAAY,OAAM,IAAI,QAAQ;AACnD,SAAO;AACT;AASO,SAAS,aACd,QACA,UAC2B;AAC3B,QAAM,IAAI,UAAU,CAAC;AACrB,QAAM,kBAAkB,EAAE;AAC1B,QAAM,iBAAiB,UAAU,eAAe,EAAE;AAClD,QAAM,WACJ,EAAE,iBAAiB,iBAAiB,kBAAkB,EAAE,eAAe,WAAW;AACpF,QAAM,YACJ,UAAU,SACV,EAAE,cACD,aAAa,YAAY,aAAa,kBAAkB,EAAE,eAAe;AAC5E,QAAM,SAAS,mBAAmB,aAAa,WAAW,EAAE,eAAe;AAC3E,MAAI,CAAC,YAAY,CAAC,aAAa,CAAC,OAAQ,QAAO;AAC/C,SAAO;AAAA,IACL,OAAO;AAAA,IACP;AAAA,IACA;AAAA,IACA,GAAI,kBAAkB,EAAE,SAAS,gBAAgB,IAAI,CAAC;AAAA,EACxD;AACF;AAEO,SAAS,eACd,SACA,SACQ;AACR,MAAI,CAAC,SAAS,OAAQ,QAAO;AAC7B,QAAM,aAAa,QAChB,IAAI,CAAC,UAAU,GAAG,MAAM,SAAS,cAAc,cAAc,MAAM,KAAK,MAAM,OAAO,EAAE,EACvF,KAAK,MAAM;AACd,SAAO,GAAG,UAAU;AAAA;AAAA,QAAa,OAAO;AAC1C;AAEO,SAAS,cACd,YACA,gBACA,OACuC;AACvC,aAAW,OAAO,OAAO,KAAK,SAAS,CAAC,CAAC,GAAG;AAC1C,QAAI,OAAO,cAAc,OAAO,gBAAgB;AAC9C,YAAM,IAAI,MAAM,iBAAiB,GAAG,gDAAgD;AAAA,IACtF;AAAA,EACF;AACA,SAAO,EAAE,GAAG,YAAY,GAAI,SAAS,CAAC,EAAG;AAC3C;AAEO,SAAS,sBACd,SACA,SACA,QACc;AACd,MAAI,CAAC,UAAU,WAAW,OAAQ,QAAO;AACzC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,YAAY;AAAA,MACV,GAAI,QAAQ,cAAc,CAAC;AAAA,MAC3B,CAAC,OAAO,GAAG;AAAA,QACT,GAAI,QAAQ,aAAa,OAAO,KAAK,CAAC;AAAA,QACtC,iBAAiB;AAAA,MACnB;AAAA,IACF;AAAA,EACF;AACF;AAmBA,gBAAuB,oBACrB,OACA,KACA,SACA,SACyB;AACzB,QAAM,UAAU,SAAS,WAAW;AACpC,QAAM,QAAQ,aAAa,MAAM,UAAU;AAAA,IACzC,OAAO,SAAS;AAAA,IAChB,aAAa,SAAS;AAAA,EACxB,CAAC;AAED,QAAM,SAAS,eAAe,SAAS,SAAS,OAAO;AAEvD,QAAM,aAAa,SAAS,cAAc,CAAC;AAC3C,QAAM,WAAW,cAAc,YAAY,SAAS,kBAAkB,CAAC,GAAG,SAAS,QAAQ;AAE3F,QAAM,UAAU,MAAM,QAAQ,EAAE,cAAc,SAAS,cAAc,SAAS,CAAC;AAC/E,QAAM,oBAAoB,sBAAsB,SAAS,SAAS,SAAS,MAAM;AAEjF,QAAM,SAAS,IAAI,aAAa,QAAQ;AAAA,IACtC,WAAW,SAAS;AAAA,IACpB,aAAa,SAAS;AAAA,IACtB,aAAa,SAAS;AAAA,IACtB,SAAS;AAAA,MACP,MAAM;AAAA,MACN,SAAS;AAAA,MACT,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IAC3B;AAAA,EACF,CAAwB;AAExB,mBAAiB,SAAS,OAAQ,OAAM;AAC1C;AAEA,eAAsB,iBACpB,OACA,KACA,SACA,SACiB;AACjB,MAAI,WAAW;AACf,MAAI,gBAAgB;AAEpB,mBAAiB,YAAY,oBAAoB,OAAO,KAAK,SAAS,OAAO,GAAG;AAC9E,UAAM,QAAQ;AACd,QAAI,CAAC,MAAM,KAAM;AAEjB,QAAI,MAAM,SAAS,wBAAwB;AACzC,YAAM,OAAO,MAAM,MAAM;AACzB,YAAM,QAAQ,OAAO,MAAM,MAAM,UAAU,WAAW,MAAM,KAAK,QAAQ;AACzE,UAAI,OAAO,MAAM,QAAQ,EAAE,MAAM,QAAQ;AACvC,YAAI,CAAC,eAAe;AAClB,0BAAgB;AAChB;AAAA,QACF;AACA,YAAI,MAAO,aAAY;AAAA,iBACd,OAAO,MAAM,SAAS,SAAU,YAAW,KAAK;AAAA,MAC3D;AAAA,IACF,WAAW,MAAM,SAAS,UAAU;AAClC,YAAM,YAAY,OAAO,MAAM,MAAM,cAAc,WAAW,MAAM,KAAK,YAAY;AACrF,UAAI,UAAW,YAAW;AAAA,IAC5B;AAAA,EACF;AAEA,SAAO;AACT;AAYA,eAAsB,qBACpB,KACA,MACA,QACA,MACwB;AACxB,MAAI;AACF,UAAM,IAAI,YAAY,IAAI,EAAE,QAAQ,MAAM,KAAK,kBAAkB,IAAI,EAAE,CAAC;AACxE,WAAO,GAAG,MAAS;AAAA,EACrB,SAAS,KAAK;AACZ,WAAO,KAAK,GAAG;AAAA,EACjB;AACF;AAEA,eAAsB,wBACpB,KACA,QACwB;AACxB,MAAI;AACF,UAAM,IAAI,YAAY,OAAO,QAAQ,EAAE,iBAAiB,KAAK,CAAC;AAC9D,WAAO,GAAG,MAAS;AAAA,EACrB,SAAS,KAAK;AACZ,WAAO,KAAK,GAAG;AAAA,EACjB;AACF;AAEA,eAAsB,sBACpB,KACA,MACA,QACA,MACwB;AACxB,MAAI;AACF,UAAM,IAAI,YAAY,OAAO,QAAQ,EAAE,MAAM,KAAK,kBAAkB,IAAI,EAAE,CAAC;AAC3E,WAAO,GAAG,MAAS;AAAA,EACrB,SAAS,KAAK;AACZ,WAAO,KAAK,GAAG;AAAA,EACjB;AACF;AASO,SAAS,sBAAsB,OAA0C;AAC9E,QAAM,SAAS,UAAU,KAAK;AAC9B,SAAO;AAAA,IACL,QAAQ,OAAO,MAAM,UAAU;AAC7B,YAAM,OAAO,QAAQ,OAAO,MAAM,KAAK;AAAA,IACzC;AAAA,IACA,QAAQ,OAAO,MAAM,UAAU;AAC7B,YAAM,OAAO,QAAQ,OAAO,MAAM,KAAK;AAAA,IACzC;AAAA,IACA,KAAK,CAAC,SAAS,OAAO,QAAQ,IAAI,IAAI;AAAA,IACtC,QAAQ,OAAO,SAAS;AACtB,YAAM,OAAO,QAAQ,OAAO,IAAI;AAAA,IAClC;AAAA,EACF;AACF;AAEA,eAAsB,YACpB,OACA,MACA,OACwB;AACxB,MAAI;AACF,UAAM,MAAM,OAAO,MAAM,KAAK;AAC9B,WAAO,GAAG,MAAS;AAAA,EACrB,QAAQ;AACN,QAAI;AACF,YAAM,MAAM,OAAO,MAAM,KAAK;AAC9B,aAAO,GAAG,MAAS;AAAA,IACrB,SAAS,KAAK;AACZ,aAAO,KAAK,IAAI,MAAM,kCAAkC,IAAI,IAAI,EAAE,OAAO,IAAI,CAAC,CAAC;AAAA,IACjF;AAAA,EACF;AACF;AAEA,eAAsB,WAAW,OAAoB,MAAwC;AAC3F,MAAI;AACF,WAAO,GAAG,MAAM,MAAM,IAAI,IAAI,CAAC;AAAA,EACjC,SAAS,KAAK;AACZ,WAAO,KAAK,GAAG;AAAA,EACjB;AACF;AAEA,eAAsB,aAAa,OAAoB,MAAsC;AAC3F,MAAI;AACF,UAAM,MAAM,OAAO,IAAI;AACvB,WAAO,GAAG,MAAS;AAAA,EACrB,SAAS,KAAK;AACZ,WAAO,KAAK,GAAG;AAAA,EACjB;AACF;AAaA,eAAsB,uBACpB,KACA,SACqC;AACrC,MAAI;AACF,UAAM,QAAQ,MAAM,IAAI,gBAAgB;AAAA,MACtC,OAAO,QAAQ;AAAA,MACf,GAAI,QAAQ,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;AAAA,MAC5D,GAAI,QAAQ,aAAa,EAAE,YAAY,QAAQ,WAAW,IAAI,CAAC;AAAA,IACjE,CAAC;AACD,WAAO,GAAG,EAAE,OAAO,MAAM,OAAO,WAAW,MAAM,WAAW,OAAO,MAAM,MAAM,CAAC;AAAA,EAClF,SAAS,KAAK;AACZ,WAAO,KAAK,GAAG;AAAA,EACjB;AACF;","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.
|
|
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": [
|
package/dist/chunk-SD2H4FWY.js
DELETED
|
@@ -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":[]}
|