@tangle-network/agent-app 0.19.0 → 0.21.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/model-resolution/index.d.ts +46 -51
- package/dist/model-resolution/index.js +43 -90
- package/dist/model-resolution/index.js.map +1 -1
- package/dist/profile/index.d.ts +4 -0
- package/dist/profile/index.js +14 -2
- package/dist/profile/index.js.map +1 -1
- package/dist/run/index.js +1 -1
- package/dist/sandbox/index.js +4 -0
- 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,
|
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* validator that sit on top of a product's boot-time model config.
|
|
2
|
+
* Canonical chat-model resolution — identical across every agent app.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The ONLY per-app inputs are DATA, never logic: the default model, the
|
|
5
|
+
* allowlist, the env value the deployment set, and the catalog-fetch loader.
|
|
6
|
+
* The logic is one precedence ladder + one fail-closed validator that every
|
|
7
|
+
* product uses the same way — there is no per-product variant, no env-var name
|
|
8
|
+
* baked in, and no backend dimension (router-vs-sandbox is the harness/dispatch
|
|
9
|
+
* concern, not model resolution; a sandbox's provider default lives in the
|
|
10
|
+
* sandbox subpath).
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* The product injects one value — `modelDefaults` — and supplies the catalog
|
|
16
|
-
* loader per call. `ModelInfo` is the router /v1/models wire shape and
|
|
17
|
-
* `canonicalModelId` the bare->prefixed id helper, both defined locally so this
|
|
18
|
-
* engine module carries no UI-package coupling.
|
|
12
|
+
* - resolveChatModel: request > workspace > env > default. The product reads its
|
|
13
|
+
* own deploy env var and passes the VALUE as `envModel`; the shell knows no
|
|
14
|
+
* env-var names. Source is canonical: 'request' | 'workspace' | 'env' | 'default'.
|
|
15
|
+
* - validateChatModelId: fail-closed. Admit an id that is in the allowlist, or
|
|
16
|
+
* equals the operator-set env model, or is served by the live router catalog
|
|
17
|
+
* (exact, or a bare id resolved to its canonical id when the suffix is unique).
|
|
19
18
|
*/
|
|
20
19
|
/** The router /v1/models entry shape this module reads. Minimal on purpose. */
|
|
21
20
|
interface ModelInfo {
|
|
@@ -24,12 +23,9 @@ interface ModelInfo {
|
|
|
24
23
|
_provider?: string;
|
|
25
24
|
provider?: string;
|
|
26
25
|
}
|
|
27
|
-
|
|
28
|
-
type ChatBackend = 'router' | 'sandbox';
|
|
29
|
-
type ChatModelSource = 'request' | 'env:MODEL_NAME' | 'default' | 'sandbox-default';
|
|
26
|
+
type ChatModelSource = 'request' | 'workspace' | 'env' | 'default';
|
|
30
27
|
interface ResolvedChatModel {
|
|
31
|
-
|
|
32
|
-
model?: string;
|
|
28
|
+
model: string;
|
|
33
29
|
source: ChatModelSource;
|
|
34
30
|
}
|
|
35
31
|
interface ChatModelValidationSuccess {
|
|
@@ -43,41 +39,40 @@ interface ChatModelValidationFailure {
|
|
|
43
39
|
type ChatModelValidationResult = ChatModelValidationSuccess | ChatModelValidationFailure;
|
|
44
40
|
/** The catalog-fetch boundary: maps a router base URL to the raw model list. */
|
|
45
41
|
type LoadModels = (routerBaseUrl: string) => Promise<ModelInfo[]>;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
sandboxOpenaiModel: string;
|
|
57
|
-
routerBaseUrl?: string;
|
|
58
|
-
extraAllowlist?: string[];
|
|
42
|
+
interface ResolveChatModelInput {
|
|
43
|
+
/** Per-request override (highest precedence). */
|
|
44
|
+
requestModel?: string;
|
|
45
|
+
/** Persisted workspace-pinned model. */
|
|
46
|
+
workspaceModel?: string;
|
|
47
|
+
/** The value the deployment's model env var holds (the product reads its own
|
|
48
|
+
* var name and passes the value — the shell stays env-var-name agnostic). */
|
|
49
|
+
envModel?: string;
|
|
50
|
+
/** Final fallback (the product's default, typically profile.model.default). */
|
|
51
|
+
defaultModel: string;
|
|
59
52
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
53
|
+
/** Resolve the chat-turn model by the one canonical precedence. Blank values are
|
|
54
|
+
* treated as absent. */
|
|
55
|
+
declare function resolveChatModel(input: ResolveChatModelInput): ResolvedChatModel;
|
|
56
|
+
interface ValidateChatModelIdInput {
|
|
57
|
+
/** Ids accepted without a catalog round-trip (defaults + operator-trusted). */
|
|
58
|
+
allowlist?: Iterable<string>;
|
|
59
|
+
/** The operator-set env model value — always admitted (operator-trusted). */
|
|
60
|
+
envModel?: string;
|
|
61
|
+
/** Catalog loader; required to reach the catalog path. */
|
|
62
|
+
loadModels?: LoadModels;
|
|
63
|
+
/** Catalog endpoint base; required to reach the catalog path. */
|
|
67
64
|
routerBaseUrl?: string;
|
|
68
|
-
/** Catalog loader. No default body is baked in; the consumer supplies it. */
|
|
69
|
-
loadModels: LoadModels;
|
|
70
|
-
}
|
|
71
|
-
interface ChatModelResolution {
|
|
72
|
-
resolveChatModel: (options: ResolveChatModelOptions) => ResolvedChatModel;
|
|
73
|
-
validateChatModelId: (modelId: unknown, options: ValidateChatModelIdOptions) => Promise<ChatModelValidationResult>;
|
|
74
|
-
DEFAULT_ROUTER_MODEL: string;
|
|
75
|
-
DEFAULT_SANDBOX_OPENAI_MODEL: string;
|
|
76
|
-
DEFAULT_ROUTER_BASE_URL?: string;
|
|
77
65
|
}
|
|
78
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Fail-closed model-id validation. Accepts an id only when it is well-formed AND
|
|
68
|
+
* (in the allowlist, or equals the operator-set env model, or served by the live
|
|
69
|
+
* catalog). A bare id (no provider prefix) resolves to its canonical id only when
|
|
70
|
+
* the suffix is unique across the catalog — an ambiguous suffix is rejected
|
|
71
|
+
* rather than silently assigned a provider.
|
|
72
|
+
*/
|
|
73
|
+
declare function validateChatModelId(modelId: unknown, input: ValidateChatModelIdInput): Promise<ChatModelValidationResult>;
|
|
79
74
|
declare function cleanModelId(value: unknown): string | undefined;
|
|
80
75
|
declare function isWellFormedModelId(modelId: string): boolean;
|
|
81
76
|
declare function catalogIdsForModel(model: ModelInfo): string[];
|
|
82
77
|
|
|
83
|
-
export { type
|
|
78
|
+
export { type ChatModelSource, type ChatModelValidationFailure, type ChatModelValidationResult, type ChatModelValidationSuccess, type LoadModels, type ModelInfo, type ResolveChatModelInput, type ResolvedChatModel, type ValidateChatModelIdInput, catalogIdsForModel, cleanModelId, isWellFormedModelId, resolveChatModel, validateChatModelId };
|
|
@@ -4,93 +4,49 @@ function canonicalModelId(model) {
|
|
|
4
4
|
const provider = model._provider ?? model.provider;
|
|
5
5
|
return provider ? `${provider}/${model.id}` : model.id;
|
|
6
6
|
}
|
|
7
|
-
function
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (backend === "router") {
|
|
26
|
-
const routerModel = cleanModelId(env.MODEL_NAME);
|
|
27
|
-
return {
|
|
28
|
-
backend,
|
|
29
|
-
model: routerModel ?? DEFAULT_ROUTER_MODEL,
|
|
30
|
-
source: routerModel ? "env:MODEL_NAME" : "default"
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
const sandboxModel = cleanModelId(env.MODEL_NAME);
|
|
34
|
-
if (sandboxModel) return { backend, model: sandboxModel, source: "env:MODEL_NAME" };
|
|
35
|
-
const modelProvider = env.MODEL_PROVIDER ?? (env.TANGLE_API_KEY ? "openai-compat" : env.OPENAI_API_KEY ? "openai" : void 0);
|
|
36
|
-
if (modelProvider === "openai" || modelProvider === "openai-compat") {
|
|
37
|
-
return { backend, model: DEFAULT_SANDBOX_OPENAI_MODEL, source: "default" };
|
|
38
|
-
}
|
|
39
|
-
return { backend, source: "sandbox-default" };
|
|
7
|
+
function resolveChatModel(input) {
|
|
8
|
+
const request = cleanModelId(input.requestModel);
|
|
9
|
+
if (request) return { model: request, source: "request" };
|
|
10
|
+
const workspace = cleanModelId(input.workspaceModel);
|
|
11
|
+
if (workspace) return { model: workspace, source: "workspace" };
|
|
12
|
+
const env = cleanModelId(input.envModel);
|
|
13
|
+
if (env) return { model: env, source: "env" };
|
|
14
|
+
return { model: input.defaultModel, source: "default" };
|
|
15
|
+
}
|
|
16
|
+
async function validateChatModelId(modelId, input) {
|
|
17
|
+
const cleaned = cleanModelId(modelId);
|
|
18
|
+
if (!cleaned) return { succeeded: false, error: "Model id must be a non-empty string." };
|
|
19
|
+
if (!isWellFormedModelId(cleaned)) return { succeeded: false, error: `Model id is malformed: ${cleaned}` };
|
|
20
|
+
const allowed = new Set(input.allowlist ?? []);
|
|
21
|
+
if (allowed.has(cleaned)) return { succeeded: true, value: cleaned };
|
|
22
|
+
if (cleanModelId(input.envModel) === cleaned) return { succeeded: true, value: cleaned };
|
|
23
|
+
if (!input.loadModels || typeof input.routerBaseUrl !== "string" || input.routerBaseUrl.length === 0) {
|
|
24
|
+
return { succeeded: false, error: `Model is not available: ${cleaned}` };
|
|
40
25
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
loadModels
|
|
44
|
-
}) {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
catalog = await loadModels(routerBaseUrl);
|
|
61
|
-
} catch (err) {
|
|
62
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
63
|
-
return { succeeded: false, error: `Could not validate model catalog: ${message}` };
|
|
64
|
-
}
|
|
65
|
-
const ids = new Set(catalog.flatMap(catalogIdsForModel));
|
|
66
|
-
if (ids.has(cleaned)) {
|
|
67
|
-
return { succeeded: true, value: cleaned };
|
|
68
|
-
}
|
|
69
|
-
if (!cleaned.includes("/")) {
|
|
70
|
-
const canonicalBySuffix = /* @__PURE__ */ new Map();
|
|
71
|
-
for (const model of catalog) {
|
|
72
|
-
const canonical = canonicalModelIdOrUndefined(model);
|
|
73
|
-
if (!canonical || !canonical.includes("/")) continue;
|
|
74
|
-
const suffix = canonical.split("/").slice(1).join("/");
|
|
75
|
-
const entries = canonicalBySuffix.get(suffix);
|
|
76
|
-
if (entries) entries.push(canonical);
|
|
77
|
-
else canonicalBySuffix.set(suffix, [canonical]);
|
|
78
|
-
}
|
|
79
|
-
const matches = canonicalBySuffix.get(cleaned);
|
|
80
|
-
const only = matches && matches.length === 1 ? matches[0] : void 0;
|
|
81
|
-
if (only) {
|
|
82
|
-
return { succeeded: true, value: only };
|
|
83
|
-
}
|
|
26
|
+
let catalog;
|
|
27
|
+
try {
|
|
28
|
+
catalog = await input.loadModels(input.routerBaseUrl);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31
|
+
return { succeeded: false, error: `Could not validate model catalog: ${message}` };
|
|
32
|
+
}
|
|
33
|
+
const ids = new Set(catalog.flatMap(catalogIdsForModel));
|
|
34
|
+
if (ids.has(cleaned)) return { succeeded: true, value: cleaned };
|
|
35
|
+
if (!cleaned.includes("/")) {
|
|
36
|
+
const canonicalBySuffix = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const model of catalog) {
|
|
38
|
+
if (typeof model.id !== "string" || !model.id.trim()) continue;
|
|
39
|
+
const canonical = canonicalModelId(model);
|
|
40
|
+
if (!canonical.includes("/")) continue;
|
|
41
|
+
const suffix = canonical.split("/").slice(1).join("/");
|
|
42
|
+
const entries = canonicalBySuffix.get(suffix);
|
|
43
|
+
if (entries) entries.push(canonical);
|
|
44
|
+
else canonicalBySuffix.set(suffix, [canonical]);
|
|
84
45
|
}
|
|
85
|
-
|
|
46
|
+
const matches = canonicalBySuffix.get(cleaned);
|
|
47
|
+
if (matches && matches.length === 1) return { succeeded: true, value: matches[0] };
|
|
86
48
|
}
|
|
87
|
-
return {
|
|
88
|
-
resolveChatModel,
|
|
89
|
-
validateChatModelId,
|
|
90
|
-
DEFAULT_ROUTER_MODEL,
|
|
91
|
-
DEFAULT_SANDBOX_OPENAI_MODEL,
|
|
92
|
-
DEFAULT_ROUTER_BASE_URL
|
|
93
|
-
};
|
|
49
|
+
return { succeeded: false, error: `Model is not available: ${cleaned}` };
|
|
94
50
|
}
|
|
95
51
|
function cleanModelId(value) {
|
|
96
52
|
if (typeof value !== "string") return void 0;
|
|
@@ -110,14 +66,11 @@ function catalogIdsForModel(model) {
|
|
|
110
66
|
}
|
|
111
67
|
return [...ids];
|
|
112
68
|
}
|
|
113
|
-
function canonicalModelIdOrUndefined(model) {
|
|
114
|
-
if (typeof model.id !== "string" || !model.id.trim()) return void 0;
|
|
115
|
-
return canonicalModelId(model);
|
|
116
|
-
}
|
|
117
69
|
export {
|
|
118
70
|
catalogIdsForModel,
|
|
119
71
|
cleanModelId,
|
|
120
|
-
|
|
121
|
-
|
|
72
|
+
isWellFormedModelId,
|
|
73
|
+
resolveChatModel,
|
|
74
|
+
validateChatModelId
|
|
122
75
|
};
|
|
123
76
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/model-resolution/index.ts"],"sourcesContent":["/**\n * Chat-time model resolution: a precedence resolver and a fail-closed catalog\n * validator that sit on top of a product's boot-time model config.\n *\n * `resolveChatModel` picks the model id for a chat turn by precedence:\n * request id > env MODEL_NAME > provider default > sandbox default.\n *\n * `validateChatModelId` is the fail-closed gate: it returns a typed outcome and\n * accepts an id only if it is in the constructed allowlist OR served by the live\n * router catalog (loaded through an injected boundary). A bare id with no\n * provider prefix resolves to its canonical id only when the suffix is unique\n * across the catalog, so an ambiguous suffix is rejected rather than silently\n * assigned a provider.\n *\n * The product injects one value — `modelDefaults` — and supplies the catalog\n * loader per call. `ModelInfo` is the router /v1/models wire shape and\n * `canonicalModelId` the bare->prefixed id helper, both defined locally so this\n * engine module carries no UI-package coupling.\n */\n\n/** The router /v1/models entry shape this module reads. Minimal on purpose. */\nexport interface ModelInfo {\n id: string\n name?: string\n _provider?: string\n provider?: string\n}\n\n/** Canonical (provider-prefixed) id for a catalog entry: pass through an id that\n * already carries a provider, else prefix the entry's provider when present. */\nfunction canonicalModelId(model: ModelInfo): string {\n if (model.id.includes('/')) return model.id\n const provider = model._provider ?? model.provider\n return provider ? `${provider}/${model.id}` : model.id\n}\n\n/** Which execution path the chat turn runs on. Product-supplied per turn. */\nexport type ChatBackend = 'router' | 'sandbox'\n\nexport type ChatModelSource =\n | 'request'\n | 'env:MODEL_NAME'\n | 'default'\n | 'sandbox-default'\n\nexport interface ResolvedChatModel {\n backend: ChatBackend\n model?: string\n source: ChatModelSource\n}\n\nexport interface ChatModelValidationSuccess {\n succeeded: true\n value: string\n}\n\nexport interface ChatModelValidationFailure {\n succeeded: false\n error: string\n}\n\nexport type ChatModelValidationResult = ChatModelValidationSuccess | ChatModelValidationFailure\n\n/** The catalog-fetch boundary: maps a router base URL to the raw model list. */\nexport type LoadModels = (routerBaseUrl: string) => Promise<ModelInfo[]>\n\n/**\n * The single product-injected seam.\n *\n * - `routerModel` / `sandboxOpenaiModel`: the two `DEFAULT_*` ids used by the\n * precedence ladder and seeded into the allowlist.\n * - `routerBaseUrl`: catalog endpoint base; overridable per validate call.\n * - `extraAllowlist`: additional ids accepted without a catalog round-trip.\n */\nexport interface ChatModelDefaults {\n routerModel: string\n sandboxOpenaiModel: string\n routerBaseUrl?: string\n extraAllowlist?: string[]\n}\n\nexport interface ResolveChatModelOptions {\n requestedModel?: string\n backend: ChatBackend\n /** Env to read (defaults to process.env). Inject for non-node runtimes. */\n env?: Record<string, string | undefined>\n}\n\nexport interface ValidateChatModelIdOptions {\n routerBaseUrl?: string\n /** Catalog loader. No default body is baked in; the consumer supplies it. */\n loadModels: LoadModels\n}\n\nexport interface ChatModelResolution {\n resolveChatModel: (options: ResolveChatModelOptions) => ResolvedChatModel\n validateChatModelId: (\n modelId: unknown,\n options: ValidateChatModelIdOptions,\n ) => Promise<ChatModelValidationResult>\n DEFAULT_ROUTER_MODEL: string\n DEFAULT_SANDBOX_OPENAI_MODEL: string\n DEFAULT_ROUTER_BASE_URL?: string\n}\n\nexport function createChatModelResolution(defaults: ChatModelDefaults): ChatModelResolution {\n const DEFAULT_ROUTER_MODEL = defaults.routerModel\n const DEFAULT_SANDBOX_OPENAI_MODEL = defaults.sandboxOpenaiModel\n const DEFAULT_ROUTER_BASE_URL = defaults.routerBaseUrl\n\n const allowlist = new Set(\n [\n DEFAULT_ROUTER_MODEL,\n DEFAULT_SANDBOX_OPENAI_MODEL,\n ...(defaults.extraAllowlist ?? []),\n ].filter((model): model is string => typeof model === 'string' && model.length > 0),\n )\n\n function resolveChatModel({\n requestedModel,\n backend,\n env = process.env,\n }: ResolveChatModelOptions): ResolvedChatModel {\n const selectedModel = cleanModelId(requestedModel)\n if (selectedModel) return { backend, model: selectedModel, source: 'request' }\n\n if (backend === 'router') {\n const routerModel = cleanModelId(env.MODEL_NAME)\n return {\n backend,\n model: routerModel ?? DEFAULT_ROUTER_MODEL,\n source: routerModel ? 'env:MODEL_NAME' : 'default',\n }\n }\n\n const sandboxModel = cleanModelId(env.MODEL_NAME)\n if (sandboxModel) return { backend, model: sandboxModel, source: 'env:MODEL_NAME' }\n\n const modelProvider = env.MODEL_PROVIDER\n ?? (env.TANGLE_API_KEY ? 'openai-compat' : env.OPENAI_API_KEY ? 'openai' : undefined)\n if (modelProvider === 'openai' || modelProvider === 'openai-compat') {\n return { backend, model: DEFAULT_SANDBOX_OPENAI_MODEL, source: 'default' }\n }\n\n return { backend, source: 'sandbox-default' }\n }\n\n async function validateChatModelId(\n modelId: unknown,\n {\n routerBaseUrl = DEFAULT_ROUTER_BASE_URL,\n loadModels,\n }: ValidateChatModelIdOptions,\n ): Promise<ChatModelValidationResult> {\n const cleaned = cleanModelId(modelId)\n if (!cleaned) {\n return { succeeded: false, error: 'Model id must be a non-empty string.' }\n }\n if (!isWellFormedModelId(cleaned)) {\n return { succeeded: false, error: `Model id is malformed: ${cleaned}` }\n }\n if (allowlist.has(cleaned)) {\n return { succeeded: true, value: cleaned }\n }\n if (typeof routerBaseUrl !== 'string' || routerBaseUrl.length === 0) {\n return { succeeded: false, error: 'Router base URL is required to validate against the catalog.' }\n }\n\n let catalog: ModelInfo[]\n try {\n catalog = await loadModels(routerBaseUrl)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Could not validate model catalog: ${message}` }\n }\n\n // Exact match against any id the catalog serves (canonical or bare).\n const ids = new Set(catalog.flatMap(catalogIdsForModel))\n if (ids.has(cleaned)) {\n return { succeeded: true, value: cleaned }\n }\n\n // A bare request id (no provider prefix) may name a model the catalog only\n // serves under a provider-prefixed id (e.g. request \"gpt-5\" -> catalog\n // \"openai/gpt-5\"). Resolve it to the canonical id the router serves, but only\n // when the bare suffix is unique across the catalog -- an ambiguous suffix\n // (e.g. \"openai/x\" vs \"vertex/x\") stays rejected so we never silently pick a\n // provider for the caller.\n if (!cleaned.includes('/')) {\n const canonicalBySuffix = new Map<string, string[]>()\n for (const model of catalog) {\n const canonical = canonicalModelIdOrUndefined(model)\n if (!canonical || !canonical.includes('/')) continue\n const suffix = canonical.split('/').slice(1).join('/')\n const entries = canonicalBySuffix.get(suffix)\n if (entries) entries.push(canonical)\n else canonicalBySuffix.set(suffix, [canonical])\n }\n const matches = canonicalBySuffix.get(cleaned)\n const only = matches && matches.length === 1 ? matches[0] : undefined\n if (only) {\n return { succeeded: true, value: only }\n }\n }\n\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n }\n\n return {\n resolveChatModel,\n validateChatModelId,\n DEFAULT_ROUTER_MODEL,\n DEFAULT_SANDBOX_OPENAI_MODEL,\n DEFAULT_ROUTER_BASE_URL,\n }\n}\n\nexport function cleanModelId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport function isWellFormedModelId(modelId: string): boolean {\n if (modelId.length > 200) return false\n return /^[A-Za-z0-9._/@:-]+$/.test(modelId)\n}\n\nexport function catalogIdsForModel(model: ModelInfo): string[] {\n const ids = new Set<string>()\n if (typeof model.id === 'string' && model.id.trim()) ids.add(model.id.trim())\n\n if (typeof model.id === 'string' && model.id.trim() && !model.id.includes('/')) {\n const canonical = canonicalModelId(model)\n if (canonical.includes('/')) ids.add(canonical)\n }\n\n // The bare suffix of a provider-prefixed id (e.g. \"openai/gpt-5\" -> \"gpt-5\")\n // is NOT added here: a bare request id resolves to its canonical id only\n // through the uniqueness-gated path in validateChatModelId, so an ambiguous\n // suffix never slips through as an exact match.\n return [...ids]\n}\n\n/** The canonical id for a catalog entry, or undefined when the entry has no id. */\nfunction canonicalModelIdOrUndefined(model: ModelInfo): string | undefined {\n if (typeof model.id !== 'string' || !model.id.trim()) return undefined\n return canonicalModelId(model)\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/model-resolution/index.ts"],"sourcesContent":["/**\n * Canonical chat-model resolution — identical across every agent app.\n *\n * The ONLY per-app inputs are DATA, never logic: the default model, the\n * allowlist, the env value the deployment set, and the catalog-fetch loader.\n * The logic is one precedence ladder + one fail-closed validator that every\n * product uses the same way — there is no per-product variant, no env-var name\n * baked in, and no backend dimension (router-vs-sandbox is the harness/dispatch\n * concern, not model resolution; a sandbox's provider default lives in the\n * sandbox subpath).\n *\n * - resolveChatModel: request > workspace > env > default. The product reads its\n * own deploy env var and passes the VALUE as `envModel`; the shell knows no\n * env-var names. Source is canonical: 'request' | 'workspace' | 'env' | 'default'.\n * - validateChatModelId: fail-closed. Admit an id that is in the allowlist, or\n * equals the operator-set env model, or is served by the live router catalog\n * (exact, or a bare id resolved to its canonical id when the suffix is unique).\n */\n\n/** The router /v1/models entry shape this module reads. Minimal on purpose. */\nexport interface ModelInfo {\n id: string\n name?: string\n _provider?: string\n provider?: string\n}\n\n/** Canonical (provider-prefixed) id for a catalog entry: pass through an id that\n * already carries a provider, else prefix the entry's provider when present. */\nfunction canonicalModelId(model: ModelInfo): string {\n if (model.id.includes('/')) return model.id\n const provider = model._provider ?? model.provider\n return provider ? `${provider}/${model.id}` : model.id\n}\n\nexport type ChatModelSource = 'request' | 'workspace' | 'env' | 'default'\n\nexport interface ResolvedChatModel {\n model: string\n source: ChatModelSource\n}\n\nexport interface ChatModelValidationSuccess {\n succeeded: true\n value: string\n}\n\nexport interface ChatModelValidationFailure {\n succeeded: false\n error: string\n}\n\nexport type ChatModelValidationResult = ChatModelValidationSuccess | ChatModelValidationFailure\n\n/** The catalog-fetch boundary: maps a router base URL to the raw model list. */\nexport type LoadModels = (routerBaseUrl: string) => Promise<ModelInfo[]>\n\nexport interface ResolveChatModelInput {\n /** Per-request override (highest precedence). */\n requestModel?: string\n /** Persisted workspace-pinned model. */\n workspaceModel?: string\n /** The value the deployment's model env var holds (the product reads its own\n * var name and passes the value — the shell stays env-var-name agnostic). */\n envModel?: string\n /** Final fallback (the product's default, typically profile.model.default). */\n defaultModel: string\n}\n\n/** Resolve the chat-turn model by the one canonical precedence. Blank values are\n * treated as absent. */\nexport function resolveChatModel(input: ResolveChatModelInput): ResolvedChatModel {\n const request = cleanModelId(input.requestModel)\n if (request) return { model: request, source: 'request' }\n const workspace = cleanModelId(input.workspaceModel)\n if (workspace) return { model: workspace, source: 'workspace' }\n const env = cleanModelId(input.envModel)\n if (env) return { model: env, source: 'env' }\n return { model: input.defaultModel, source: 'default' }\n}\n\nexport interface ValidateChatModelIdInput {\n /** Ids accepted without a catalog round-trip (defaults + operator-trusted). */\n allowlist?: Iterable<string>\n /** The operator-set env model value — always admitted (operator-trusted). */\n envModel?: string\n /** Catalog loader; required to reach the catalog path. */\n loadModels?: LoadModels\n /** Catalog endpoint base; required to reach the catalog path. */\n routerBaseUrl?: string\n}\n\n/**\n * Fail-closed model-id validation. Accepts an id only when it is well-formed AND\n * (in the allowlist, or equals the operator-set env model, or served by the live\n * catalog). A bare id (no provider prefix) resolves to its canonical id only when\n * the suffix is unique across the catalog — an ambiguous suffix is rejected\n * rather than silently assigned a provider.\n */\nexport async function validateChatModelId(\n modelId: unknown,\n input: ValidateChatModelIdInput,\n): Promise<ChatModelValidationResult> {\n const cleaned = cleanModelId(modelId)\n if (!cleaned) return { succeeded: false, error: 'Model id must be a non-empty string.' }\n if (!isWellFormedModelId(cleaned)) return { succeeded: false, error: `Model id is malformed: ${cleaned}` }\n\n const allowed = new Set(input.allowlist ?? [])\n if (allowed.has(cleaned)) return { succeeded: true, value: cleaned }\n\n // The operator-set env model is trusted without a catalog round-trip.\n if (cleanModelId(input.envModel) === cleaned) return { succeeded: true, value: cleaned }\n\n if (!input.loadModels || typeof input.routerBaseUrl !== 'string' || input.routerBaseUrl.length === 0) {\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n }\n\n let catalog: ModelInfo[]\n try {\n catalog = await input.loadModels(input.routerBaseUrl)\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n return { succeeded: false, error: `Could not validate model catalog: ${message}` }\n }\n\n const ids = new Set(catalog.flatMap(catalogIdsForModel))\n if (ids.has(cleaned)) return { succeeded: true, value: cleaned }\n\n if (!cleaned.includes('/')) {\n const canonicalBySuffix = new Map<string, string[]>()\n for (const model of catalog) {\n if (typeof model.id !== 'string' || !model.id.trim()) continue\n const canonical = canonicalModelId(model)\n if (!canonical.includes('/')) continue\n const suffix = canonical.split('/').slice(1).join('/')\n const entries = canonicalBySuffix.get(suffix)\n if (entries) entries.push(canonical)\n else canonicalBySuffix.set(suffix, [canonical])\n }\n const matches = canonicalBySuffix.get(cleaned)\n if (matches && matches.length === 1) return { succeeded: true, value: matches[0]! }\n }\n\n return { succeeded: false, error: `Model is not available: ${cleaned}` }\n}\n\nexport function cleanModelId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined\n const trimmed = value.trim()\n return trimmed.length > 0 ? trimmed : undefined\n}\n\nexport function isWellFormedModelId(modelId: string): boolean {\n if (modelId.length > 200) return false\n return /^[A-Za-z0-9._/@:-]+$/.test(modelId)\n}\n\nexport function catalogIdsForModel(model: ModelInfo): string[] {\n const ids = new Set<string>()\n if (typeof model.id === 'string' && model.id.trim()) ids.add(model.id.trim())\n if (typeof model.id === 'string' && model.id.trim() && !model.id.includes('/')) {\n const canonical = canonicalModelId(model)\n if (canonical.includes('/')) ids.add(canonical)\n }\n return [...ids]\n}\n"],"mappings":"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","names":[]}
|
package/dist/profile/index.d.ts
CHANGED
|
@@ -82,6 +82,10 @@ interface ProfileOverlay {
|
|
|
82
82
|
* `prompt.systemPrompt` while keeping base `prompt.instructions`. When unset,
|
|
83
83
|
* the base prompt passes through unchanged. */
|
|
84
84
|
systemPrompt?: string;
|
|
85
|
+
/** Extra instruction lines merged onto the active prompt (e.g. a per-turn
|
|
86
|
+
* domain/integration directive). Appended to base `prompt.instructions` by
|
|
87
|
+
* the SDK merge. */
|
|
88
|
+
instructions?: string[];
|
|
85
89
|
/** Profile `name` override. When unset, the base name is kept. */
|
|
86
90
|
name?: string;
|
|
87
91
|
}
|
package/dist/profile/index.js
CHANGED
|
@@ -29,16 +29,28 @@ function composeAgentProfile(base, channels = {}, overlay = {}) {
|
|
|
29
29
|
const userFiles = channels.userSkills ? userSkillMounts(channels.userSkills) : [];
|
|
30
30
|
const overlayFiles = channels.filesPredicate ? userFiles.filter(channels.filesPredicate) : userFiles;
|
|
31
31
|
const files = [...channelFiles, ...overlayFiles];
|
|
32
|
+
const promptOverlay = {};
|
|
33
|
+
if (overlay.systemPrompt) promptOverlay.systemPrompt = overlay.systemPrompt;
|
|
34
|
+
if (overlay.instructions && overlay.instructions.length > 0) promptOverlay.instructions = overlay.instructions;
|
|
32
35
|
const overlayProfile = {
|
|
33
36
|
...overlay.name ? { name: overlay.name } : {},
|
|
34
|
-
...
|
|
37
|
+
...Object.keys(promptOverlay).length > 0 ? { prompt: promptOverlay } : {},
|
|
35
38
|
...overlay.mcp ? { mcp: overlay.mcp } : {},
|
|
36
39
|
resources: { files }
|
|
37
40
|
};
|
|
38
41
|
const merged = mergeAgentProfiles(base, overlayProfile);
|
|
39
42
|
if (!merged)
|
|
40
43
|
throw new Error("composeAgentProfile: mergeAgentProfiles returned undefined for a defined base");
|
|
41
|
-
return merged;
|
|
44
|
+
return pruneEmptyResourceChannels(merged);
|
|
45
|
+
}
|
|
46
|
+
function pruneEmptyResourceChannels(profile2) {
|
|
47
|
+
if (!profile2.resources) return profile2;
|
|
48
|
+
const kept = Object.fromEntries(
|
|
49
|
+
Object.entries(profile2.resources).filter(([, value]) => !(Array.isArray(value) && value.length === 0))
|
|
50
|
+
);
|
|
51
|
+
const out = { ...profile2, resources: kept };
|
|
52
|
+
if (kept && Object.keys(kept).length === 0) delete out.resources;
|
|
53
|
+
return out;
|
|
42
54
|
}
|
|
43
55
|
function stripComments(raw) {
|
|
44
56
|
return raw.replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/profile/index.ts"],"sourcesContent":["/**\n * Profile composer + evolvable-section seam for agent products.\n *\n * The standard \"load a deployable AgentProfile, including skills, plus the\n * skills the end user added to their own instance\" entry point. A product holds\n * a canonical base `AgentProfile` (role/environment/tool-conventions rendered\n * into `prompt.systemPrompt`, baseline skills, baseline MCP). At deploy/turn\n * time it layers four file-mount channels onto `resources.files` —\n *\n * 1. skills — the always-mounted product skill corpus\n * 2. knowledge — a second always-mounted corpus (domain knowledge pack)\n * 3. registry — the tier-gated installable registry (free -> boot-mounted)\n * 4. userSkills — per-user / per-workspace skills the END USER adds to their\n * own instance, mounted at `~/.claude/skills/<id>/SKILL.md`\n * exactly like the registry's free tier\n *\n * plus an optional MCP overlay (delegation + per-turn app-tool side channel), a\n * per-turn `systemPrompt` override, and a `name` override. The merge is the SDK\n * `mergeAgentProfiles`: `mcp` is last-wins per key (base -> overlay), `resources`\n * arrays are concatenated (base ++ overlay), `prompt` is shallow-merged so an\n * overlay carrying only `systemPrompt` overrides it while keeping base\n * instructions. The compose algebra is DATA — the product injects the base\n * profile, the channel mounts (built with the `skills` subpath primitives), the\n * delegation/app-tool MCP map, and the override strings; nothing here reaches\n * for env, a glob, or a specific product's profile.\n *\n * The evolvable-section seam is the loader closure. A product's single\n * self-improvable domain section (the one `applyDomainPatch` targets) loads its\n * body from a deployed markdown override, falling back to an in-tree baseline.\n * The `import.meta.glob('<lit>', ...)` literal must stay at the CONSUMER call\n * site (Vite static-analyzes it), so `makeEvolvableSection` takes the loader as\n * a closure and a REQUIRED `baseline` — it never constructs a glob and never\n * defaults the baseline, so a product can't render an empty learned-guidance\n * section. `stripComments` is the shared \"is this addendum really empty?\" test.\n */\n\nimport type {\n AgentProfile,\n AgentProfileFileMount,\n AgentProfileMcpServer,\n} from '@tangle-network/sandbox'\nimport { mergeAgentProfiles } from '@tangle-network/sandbox'\nimport { profile } from '@tangle-network/agent-eval'\nimport {\n composeShellResources,\n registrySkills,\n skillMountPath,\n type ComposeShellResourcesInput,\n type SkillEntry,\n} from '../skills/index'\n\n/** Re-expose the agent-eval section/render substrate so a product wires the\n * evolvable surface through ONE subpath: `makeEvolvableSection` builds the\n * section, `profile.renderProfile` renders it, `profile.applyDomainPatch` lets\n * the loop patch it by id. The rendering/patching engine stays in agent-eval;\n * reach it through this namespace (re-exporting the bare fns would leak\n * agent-eval's un-nameable AgentProfile type into our generated d.ts). */\nexport { profile }\n\n/** The file-mount channels layered onto `resources.files`. The first three\n * mirror {@link ComposeShellResourcesInput}; `userSkills` is the per-user /\n * per-workspace channel — skills the END USER added to their own instance,\n * mounted at the harness skill-discovery path like the registry's free tier. */\nexport interface ProfileChannels {\n /** Always-mounted skill corpus (pass `corpusSkills(...)`). */\n skills?: AgentProfileFileMount[]\n /** Always-mounted knowledge corpus (pass `corpusSkills(...)` for the pack). */\n knowledge?: AgentProfileFileMount[]\n /** Single-file evolvable / learned-guidance corpora, if mounted as files. */\n evolvable?: AgentProfileFileMount[]\n /** Tier-gated installable registry (pass the registry array; free tier is\n * mounted, paid is install-on-demand). Gated through {@link registrySkills}. */\n registry?: SkillEntry[]\n /** Per-user / per-workspace skills the end user adds to their own instance.\n * Mounted at `~/.claude/skills/<id>/SKILL.md`, the same harness path the\n * registry uses, so a user skill and a registry skill with the same id\n * collide deterministically (the user skill, appended last, wins). */\n userSkills?: UserSkill[]\n /** Final skip filter applied to the composed mount list by mount `path`. */\n filesPredicate?: (mount: AgentProfileFileMount) => boolean\n}\n\n/** A per-user / per-workspace skill: an id and an inline `SKILL.md` body. The\n * user-facing analogue of a registry {@link SkillEntry} with no tier gate —\n * every user skill is mounted (the user opted in by adding it). */\nexport interface UserSkill {\n id: string\n /** Inline `SKILL.md` body mounted at {@link skillMountPath}. */\n skillMd: string\n}\n\n/** Overlay overrides applied on top of the channel mounts. */\nexport interface ProfileOverlay {\n /** Extra MCP servers merged into the profile `mcp` map (last-wins per key over\n * the base servers). The product builds this from its delegation MCP entry\n * and any per-turn app-tool side-channel servers. An absent/`undefined` entry\n * is dropped — pass only the servers that resolved (fail-closed at the seam,\n * not here). */\n mcp?: Record<string, AgentProfileMcpServer>\n /** Per-turn system-prompt override. When set, replaces the base\n * `prompt.systemPrompt` while keeping base `prompt.instructions`. When unset,\n * the base prompt passes through unchanged. */\n systemPrompt?: string\n /** Profile `name` override. When unset, the base name is kept. */\n name?: string\n}\n\n/** Project per-user skills onto SDK file mounts at the harness skill-discovery\n * path. No tier gate — a user skill is mounted because the user added it.\n * Sorted by path for determinism (matches {@link registrySkills}). */\nexport function userSkillMounts(userSkills: UserSkill[]): AgentProfileFileMount[] {\n return userSkills\n .map(\n (s) =>\n ({\n path: skillMountPath(s.id),\n resource: { kind: 'inline', name: s.id, content: s.skillMd },\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/**\n * Compose a deployable `AgentProfile` from a canonical base plus the four\n * file-mount channels and the overlay overrides.\n *\n * Files: base `resources.files` come first; the four channels follow in\n * `skills -> knowledge -> evolvable -> registry -> userSkills` order (so a\n * userSkill that mounts at the same path as a registry skill is the last write\n * and wins). MCP: base servers first, the overlay `mcp` last (last-wins per\n * key). Prompt: the overlay `systemPrompt`, when set, replaces the base one;\n * base instructions are preserved. Name: the overlay `name`, when set, wins.\n *\n * The merge delegates to the SDK `mergeAgentProfiles` (overlay-wins on records,\n * arrays concatenated) — the deterministic algebra is the overlay we hand it,\n * not a hand-rolled spread. `mergeAgentProfiles(base, overlay)` returns\n * `undefined` only when BOTH are `undefined`; `base` is always defined here, so\n * the result is non-`undefined` by construction and we assert that to the caller.\n */\nexport function composeAgentProfile(\n base: AgentProfile,\n channels: ProfileChannels = {},\n overlay: ProfileOverlay = {},\n): AgentProfile {\n const shellInput: ComposeShellResourcesInput = {\n skills: channels.skills,\n knowledge: channels.knowledge,\n evolvable: channels.evolvable,\n registry: channels.registry ? registrySkills(channels.registry) : undefined,\n predicate: channels.filesPredicate,\n }\n const channelFiles = composeShellResources(shellInput)\n const userFiles = channels.userSkills ? userSkillMounts(channels.userSkills) : []\n const overlayFiles = channels.filesPredicate\n ? userFiles.filter(channels.filesPredicate)\n : userFiles\n const files = [...channelFiles, ...overlayFiles]\n\n const overlayProfile: AgentProfile = {\n ...(overlay.name ? { name: overlay.name } : {}),\n ...(overlay.systemPrompt ? { prompt: { systemPrompt: overlay.systemPrompt } } : {}),\n ...(overlay.mcp ? { mcp: overlay.mcp } : {}),\n resources: { files },\n }\n\n const merged = mergeAgentProfiles(base, overlayProfile)\n if (!merged)\n throw new Error('composeAgentProfile: mergeAgentProfiles returned undefined for a defined base')\n return merged\n}\n\n/** True body of an addendum file with HTML comments stripped — an all-comment\n * placeholder counts as empty, so the loader falls back to the baseline. */\nexport function stripComments(raw: string): string {\n return raw.replace(/<!--[\\s\\S]*?-->/g, '').trim()\n}\n\n/** Inputs to {@link makeEvolvableSection}. */\nexport interface EvolvableSectionInput {\n /** Section id the self-improvement loop targets with `applyDomainPatch`. */\n id: string\n /** Section title rendered as `### <title>`. */\n title: string\n /**\n * Load the deployed section body. The CONSUMER supplies this closure and runs\n * its own `import.meta.glob('<lit>', { eager: true, query: '?raw', import:\n * 'default' })` inside it — the literal must stay at the call site so Vite can\n * static-analyze it; a glob constructed here would not resolve the product's\n * files. Return the raw markdown (comments and all); `makeEvolvableSection`\n * applies {@link stripComments} to decide whether it is really populated.\n */\n load: () => string\n /**\n * The in-tree fallback body, used when `load()` returns an\n * all-comments/empty placeholder. REQUIRED — no internal default — so a\n * product can never accidentally render an empty evolvable section.\n */\n baseline: string\n}\n\n/**\n * Build the one evolvable (`evolvable: true`) domain section whose body comes\n * from the product's loader, falling back to the required baseline when the\n * loaded body is empty after stripping comments. Returns the agent-eval\n * `AgentProfileSection` shape — drop it straight into `prodProfile`'s shipped\n * sections. The loader is the only seam; the empty-vs-populated rule and the\n * baseline fallback are the lifted algebra.\n */\nexport function makeEvolvableSection(input: EvolvableSectionInput): profile.AgentProfileSection {\n const loaded = input.load()\n const body = stripComments(loaded) ? loaded.trim() : input.baseline\n return { id: input.id, title: input.title, body, evolvable: true }\n}\n\nexport {\n composeShellResources,\n corpusSkills,\n loadMarkdownCorpus,\n registrySkills,\n skillMountPath,\n} from '../skills/index'\nexport type {\n ComposeShellResourcesInput,\n CorpusEntry,\n CorpusLoadResult,\n GlobModules,\n LoadCorpusOptions,\n SkillEntry,\n} from '../skills/index'\n"],"mappings":";;;;;;;;;AAyCA,SAAS,0BAA0B;AACnC,SAAS,eAAe;AAoEjB,SAAS,gBAAgB,YAAkD;AAChF,SAAO,WACJ;AAAA,IACC,CAAC,OACE;AAAA,MACC,MAAM,eAAe,EAAE,EAAE;AAAA,MACzB,UAAU,EAAE,MAAM,UAAU,MAAM,EAAE,IAAI,SAAS,EAAE,QAAQ;AAAA,IAC7D;AAAA,EACJ,EACC,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAChD;AAmBO,SAAS,oBACd,MACA,WAA4B,CAAC,GAC7B,UAA0B,CAAC,GACb;AACd,QAAM,aAAyC;AAAA,IAC7C,QAAQ,SAAS;AAAA,IACjB,WAAW,SAAS;AAAA,IACpB,WAAW,SAAS;AAAA,IACpB,UAAU,SAAS,WAAW,eAAe,SAAS,QAAQ,IAAI;AAAA,IAClE,WAAW,SAAS;AAAA,EACtB;AACA,QAAM,eAAe,sBAAsB,UAAU;AACrD,QAAM,YAAY,SAAS,aAAa,gBAAgB,SAAS,UAAU,IAAI,CAAC;AAChF,QAAM,eAAe,SAAS,iBAC1B,UAAU,OAAO,SAAS,cAAc,IACxC;AACJ,QAAM,QAAQ,CAAC,GAAG,cAAc,GAAG,YAAY;AAE/C,QAAM,iBAA+B;AAAA,IACnC,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC7C,GAAI,QAAQ,eAAe,EAAE,QAAQ,EAAE,cAAc,QAAQ,aAAa,EAAE,IAAI,CAAC;AAAA,IACjF,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,IAC1C,WAAW,EAAE,MAAM;AAAA,EACrB;AAEA,QAAM,SAAS,mBAAmB,MAAM,cAAc;AACtD,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,+EAA+E;AACjG,SAAO;AACT;AAIO,SAAS,cAAc,KAAqB;AACjD,SAAO,IAAI,QAAQ,oBAAoB,EAAE,EAAE,KAAK;AAClD;AAiCO,SAAS,qBAAqB,OAA2D;AAC9F,QAAM,SAAS,MAAM,KAAK;AAC1B,QAAM,OAAO,cAAc,MAAM,IAAI,OAAO,KAAK,IAAI,MAAM;AAC3D,SAAO,EAAE,IAAI,MAAM,IAAI,OAAO,MAAM,OAAO,MAAM,WAAW,KAAK;AACnE;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/profile/index.ts"],"sourcesContent":["/**\n * Profile composer + evolvable-section seam for agent products.\n *\n * The standard \"load a deployable AgentProfile, including skills, plus the\n * skills the end user added to their own instance\" entry point. A product holds\n * a canonical base `AgentProfile` (role/environment/tool-conventions rendered\n * into `prompt.systemPrompt`, baseline skills, baseline MCP). At deploy/turn\n * time it layers four file-mount channels onto `resources.files` —\n *\n * 1. skills — the always-mounted product skill corpus\n * 2. knowledge — a second always-mounted corpus (domain knowledge pack)\n * 3. registry — the tier-gated installable registry (free -> boot-mounted)\n * 4. userSkills — per-user / per-workspace skills the END USER adds to their\n * own instance, mounted at `~/.claude/skills/<id>/SKILL.md`\n * exactly like the registry's free tier\n *\n * plus an optional MCP overlay (delegation + per-turn app-tool side channel), a\n * per-turn `systemPrompt` override, and a `name` override. The merge is the SDK\n * `mergeAgentProfiles`: `mcp` is last-wins per key (base -> overlay), `resources`\n * arrays are concatenated (base ++ overlay), `prompt` is shallow-merged so an\n * overlay carrying only `systemPrompt` overrides it while keeping base\n * instructions. The compose algebra is DATA — the product injects the base\n * profile, the channel mounts (built with the `skills` subpath primitives), the\n * delegation/app-tool MCP map, and the override strings; nothing here reaches\n * for env, a glob, or a specific product's profile.\n *\n * The evolvable-section seam is the loader closure. A product's single\n * self-improvable domain section (the one `applyDomainPatch` targets) loads its\n * body from a deployed markdown override, falling back to an in-tree baseline.\n * The `import.meta.glob('<lit>', ...)` literal must stay at the CONSUMER call\n * site (Vite static-analyzes it), so `makeEvolvableSection` takes the loader as\n * a closure and a REQUIRED `baseline` — it never constructs a glob and never\n * defaults the baseline, so a product can't render an empty learned-guidance\n * section. `stripComments` is the shared \"is this addendum really empty?\" test.\n */\n\nimport type {\n AgentProfile,\n AgentProfileFileMount,\n AgentProfileMcpServer,\n} from '@tangle-network/sandbox'\nimport { mergeAgentProfiles } from '@tangle-network/sandbox'\nimport { profile } from '@tangle-network/agent-eval'\nimport {\n composeShellResources,\n registrySkills,\n skillMountPath,\n type ComposeShellResourcesInput,\n type SkillEntry,\n} from '../skills/index'\n\n/** Re-expose the agent-eval section/render substrate so a product wires the\n * evolvable surface through ONE subpath: `makeEvolvableSection` builds the\n * section, `profile.renderProfile` renders it, `profile.applyDomainPatch` lets\n * the loop patch it by id. The rendering/patching engine stays in agent-eval;\n * reach it through this namespace (re-exporting the bare fns would leak\n * agent-eval's un-nameable AgentProfile type into our generated d.ts). */\nexport { profile }\n\n/** The file-mount channels layered onto `resources.files`. The first three\n * mirror {@link ComposeShellResourcesInput}; `userSkills` is the per-user /\n * per-workspace channel — skills the END USER added to their own instance,\n * mounted at the harness skill-discovery path like the registry's free tier. */\nexport interface ProfileChannels {\n /** Always-mounted skill corpus (pass `corpusSkills(...)`). */\n skills?: AgentProfileFileMount[]\n /** Always-mounted knowledge corpus (pass `corpusSkills(...)` for the pack). */\n knowledge?: AgentProfileFileMount[]\n /** Single-file evolvable / learned-guidance corpora, if mounted as files. */\n evolvable?: AgentProfileFileMount[]\n /** Tier-gated installable registry (pass the registry array; free tier is\n * mounted, paid is install-on-demand). Gated through {@link registrySkills}. */\n registry?: SkillEntry[]\n /** Per-user / per-workspace skills the end user adds to their own instance.\n * Mounted at `~/.claude/skills/<id>/SKILL.md`, the same harness path the\n * registry uses, so a user skill and a registry skill with the same id\n * collide deterministically (the user skill, appended last, wins). */\n userSkills?: UserSkill[]\n /** Final skip filter applied to the composed mount list by mount `path`. */\n filesPredicate?: (mount: AgentProfileFileMount) => boolean\n}\n\n/** A per-user / per-workspace skill: an id and an inline `SKILL.md` body. The\n * user-facing analogue of a registry {@link SkillEntry} with no tier gate —\n * every user skill is mounted (the user opted in by adding it). */\nexport interface UserSkill {\n id: string\n /** Inline `SKILL.md` body mounted at {@link skillMountPath}. */\n skillMd: string\n}\n\n/** Overlay overrides applied on top of the channel mounts. */\nexport interface ProfileOverlay {\n /** Extra MCP servers merged into the profile `mcp` map (last-wins per key over\n * the base servers). The product builds this from its delegation MCP entry\n * and any per-turn app-tool side-channel servers. An absent/`undefined` entry\n * is dropped — pass only the servers that resolved (fail-closed at the seam,\n * not here). */\n mcp?: Record<string, AgentProfileMcpServer>\n /** Per-turn system-prompt override. When set, replaces the base\n * `prompt.systemPrompt` while keeping base `prompt.instructions`. When unset,\n * the base prompt passes through unchanged. */\n systemPrompt?: string\n /** Extra instruction lines merged onto the active prompt (e.g. a per-turn\n * domain/integration directive). Appended to base `prompt.instructions` by\n * the SDK merge. */\n instructions?: string[]\n /** Profile `name` override. When unset, the base name is kept. */\n name?: string\n}\n\n/** Project per-user skills onto SDK file mounts at the harness skill-discovery\n * path. No tier gate — a user skill is mounted because the user added it.\n * Sorted by path for determinism (matches {@link registrySkills}). */\nexport function userSkillMounts(userSkills: UserSkill[]): AgentProfileFileMount[] {\n return userSkills\n .map(\n (s) =>\n ({\n path: skillMountPath(s.id),\n resource: { kind: 'inline', name: s.id, content: s.skillMd },\n }) satisfies AgentProfileFileMount,\n )\n .sort((a, b) => a.path.localeCompare(b.path))\n}\n\n/**\n * Compose a deployable `AgentProfile` from a canonical base plus the four\n * file-mount channels and the overlay overrides.\n *\n * Files: base `resources.files` come first; the four channels follow in\n * `skills -> knowledge -> evolvable -> registry -> userSkills` order (so a\n * userSkill that mounts at the same path as a registry skill is the last write\n * and wins). MCP: base servers first, the overlay `mcp` last (last-wins per\n * key). Prompt: the overlay `systemPrompt`, when set, replaces the base one;\n * base instructions are preserved. Name: the overlay `name`, when set, wins.\n *\n * The merge delegates to the SDK `mergeAgentProfiles` (overlay-wins on records,\n * arrays concatenated) — the deterministic algebra is the overlay we hand it,\n * not a hand-rolled spread. `mergeAgentProfiles(base, overlay)` returns\n * `undefined` only when BOTH are `undefined`; `base` is always defined here, so\n * the result is non-`undefined` by construction and we assert that to the caller.\n */\nexport function composeAgentProfile(\n base: AgentProfile,\n channels: ProfileChannels = {},\n overlay: ProfileOverlay = {},\n): AgentProfile {\n const shellInput: ComposeShellResourcesInput = {\n skills: channels.skills,\n knowledge: channels.knowledge,\n evolvable: channels.evolvable,\n registry: channels.registry ? registrySkills(channels.registry) : undefined,\n predicate: channels.filesPredicate,\n }\n const channelFiles = composeShellResources(shellInput)\n const userFiles = channels.userSkills ? userSkillMounts(channels.userSkills) : []\n const overlayFiles = channels.filesPredicate\n ? userFiles.filter(channels.filesPredicate)\n : userFiles\n const files = [...channelFiles, ...overlayFiles]\n\n const promptOverlay: { systemPrompt?: string; instructions?: string[] } = {}\n if (overlay.systemPrompt) promptOverlay.systemPrompt = overlay.systemPrompt\n if (overlay.instructions && overlay.instructions.length > 0) promptOverlay.instructions = overlay.instructions\n\n const overlayProfile: AgentProfile = {\n ...(overlay.name ? { name: overlay.name } : {}),\n ...(Object.keys(promptOverlay).length > 0 ? { prompt: promptOverlay } : {}),\n ...(overlay.mcp ? { mcp: overlay.mcp } : {}),\n resources: { files },\n }\n\n const merged = mergeAgentProfiles(base, overlayProfile)\n if (!merged)\n throw new Error('composeAgentProfile: mergeAgentProfiles returned undefined for a defined base')\n return pruneEmptyResourceChannels(merged)\n}\n\n/** Drop empty resource channels the SDK merge normalizes in (`tools`/`skills`/\n * `agents`/`commands`: `[]`), so the composed profile's wire payload carries\n * only the channels that actually have content — one canonical shape every app\n * emits, instead of a sidecar payload full of empty arrays. */\nfunction pruneEmptyResourceChannels(profile: AgentProfile): AgentProfile {\n if (!profile.resources) return profile\n const kept = Object.fromEntries(\n Object.entries(profile.resources).filter(([, value]) => !(Array.isArray(value) && value.length === 0)),\n ) as AgentProfile['resources']\n const out: AgentProfile = { ...profile, resources: kept }\n if (kept && Object.keys(kept).length === 0) delete out.resources\n return out\n}\n\n/** True body of an addendum file with HTML comments stripped — an all-comment\n * placeholder counts as empty, so the loader falls back to the baseline. */\nexport function stripComments(raw: string): string {\n return raw.replace(/<!--[\\s\\S]*?-->/g, '').trim()\n}\n\n/** Inputs to {@link makeEvolvableSection}. */\nexport interface EvolvableSectionInput {\n /** Section id the self-improvement loop targets with `applyDomainPatch`. */\n id: string\n /** Section title rendered as `### <title>`. */\n title: string\n /**\n * Load the deployed section body. The CONSUMER supplies this closure and runs\n * its own `import.meta.glob('<lit>', { eager: true, query: '?raw', import:\n * 'default' })` inside it — the literal must stay at the call site so Vite can\n * static-analyze it; a glob constructed here would not resolve the product's\n * files. Return the raw markdown (comments and all); `makeEvolvableSection`\n * applies {@link stripComments} to decide whether it is really populated.\n */\n load: () => string\n /**\n * The in-tree fallback body, used when `load()` returns an\n * all-comments/empty placeholder. REQUIRED — no internal default — so a\n * product can never accidentally render an empty evolvable section.\n */\n baseline: string\n}\n\n/**\n * Build the one evolvable (`evolvable: true`) domain section whose body comes\n * from the product's loader, falling back to the required baseline when the\n * loaded body is empty after stripping comments. Returns the agent-eval\n * `AgentProfileSection` shape — drop it straight into `prodProfile`'s shipped\n * sections. The loader is the only seam; the empty-vs-populated rule and the\n * baseline fallback are the lifted algebra.\n */\nexport function makeEvolvableSection(input: EvolvableSectionInput): profile.AgentProfileSection {\n const loaded = input.load()\n const body = stripComments(loaded) ? loaded.trim() : input.baseline\n return { id: input.id, title: input.title, body, evolvable: true }\n}\n\nexport {\n composeShellResources,\n corpusSkills,\n loadMarkdownCorpus,\n registrySkills,\n skillMountPath,\n} from '../skills/index'\nexport type {\n ComposeShellResourcesInput,\n CorpusEntry,\n CorpusLoadResult,\n GlobModules,\n LoadCorpusOptions,\n SkillEntry,\n} from '../skills/index'\n"],"mappings":";;;;;;;;;AAyCA,SAAS,0BAA0B;AACnC,SAAS,eAAe;AAwEjB,SAAS,gBAAgB,YAAkD;AAChF,SAAO,WACJ;AAAA,IACC,CAAC,OACE;AAAA,MACC,MAAM,eAAe,EAAE,EAAE;AAAA,MACzB,UAAU,EAAE,MAAM,UAAU,MAAM,EAAE,IAAI,SAAS,EAAE,QAAQ;AAAA,IAC7D;AAAA,EACJ,EACC,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAChD;AAmBO,SAAS,oBACd,MACA,WAA4B,CAAC,GAC7B,UAA0B,CAAC,GACb;AACd,QAAM,aAAyC;AAAA,IAC7C,QAAQ,SAAS;AAAA,IACjB,WAAW,SAAS;AAAA,IACpB,WAAW,SAAS;AAAA,IACpB,UAAU,SAAS,WAAW,eAAe,SAAS,QAAQ,IAAI;AAAA,IAClE,WAAW,SAAS;AAAA,EACtB;AACA,QAAM,eAAe,sBAAsB,UAAU;AACrD,QAAM,YAAY,SAAS,aAAa,gBAAgB,SAAS,UAAU,IAAI,CAAC;AAChF,QAAM,eAAe,SAAS,iBAC1B,UAAU,OAAO,SAAS,cAAc,IACxC;AACJ,QAAM,QAAQ,CAAC,GAAG,cAAc,GAAG,YAAY;AAE/C,QAAM,gBAAoE,CAAC;AAC3E,MAAI,QAAQ,aAAc,eAAc,eAAe,QAAQ;AAC/D,MAAI,QAAQ,gBAAgB,QAAQ,aAAa,SAAS,EAAG,eAAc,eAAe,QAAQ;AAElG,QAAM,iBAA+B;AAAA,IACnC,GAAI,QAAQ,OAAO,EAAE,MAAM,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC7C,GAAI,OAAO,KAAK,aAAa,EAAE,SAAS,IAAI,EAAE,QAAQ,cAAc,IAAI,CAAC;AAAA,IACzE,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,IAC1C,WAAW,EAAE,MAAM;AAAA,EACrB;AAEA,QAAM,SAAS,mBAAmB,MAAM,cAAc;AACtD,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,+EAA+E;AACjG,SAAO,2BAA2B,MAAM;AAC1C;AAMA,SAAS,2BAA2BA,UAAqC;AACvE,MAAI,CAACA,SAAQ,UAAW,QAAOA;AAC/B,QAAM,OAAO,OAAO;AAAA,IAClB,OAAO,QAAQA,SAAQ,SAAS,EAAE,OAAO,CAAC,CAAC,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,KAAK,KAAK,MAAM,WAAW,EAAE;AAAA,EACvG;AACA,QAAM,MAAoB,EAAE,GAAGA,UAAS,WAAW,KAAK;AACxD,MAAI,QAAQ,OAAO,KAAK,IAAI,EAAE,WAAW,EAAG,QAAO,IAAI;AACvD,SAAO;AACT;AAIO,SAAS,cAAc,KAAqB;AACjD,SAAO,IAAI,QAAQ,oBAAoB,EAAE,EAAE,KAAK;AAClD;AAiCO,SAAS,qBAAqB,OAA2D;AAC9F,QAAM,SAAS,MAAM,KAAK;AAC1B,QAAM,OAAO,cAAc,MAAM,IAAI,OAAO,KAAK,IAAI,MAAM;AAC3D,SAAO,EAAE,IAAI,MAAM,IAAI,OAAO,MAAM,OAAO,MAAM,WAAW,KAAK;AACnE;","names":["profile"]}
|
package/dist/run/index.js
CHANGED
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
|
|
@@ -160,6 +163,7 @@ async function* streamSandboxPrompt(shell, box, message, options) {
|
|
|
160
163
|
model: options?.model,
|
|
161
164
|
modelApiKey: options?.modelApiKey
|
|
162
165
|
});
|
|
166
|
+
if (model?.model) assertHarnessModelCompatible(harness, model.model);
|
|
163
167
|
const prompt = flattenHistory(message, options?.history);
|
|
164
168
|
const appToolMcp = options?.appToolMcp ?? {};
|
|
165
169
|
const extraMcp = mergeExtraMcp(appToolMcp, options?.baseProfileMcp ?? {}, options?.extraMcp);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/sandbox/index.ts"],"sourcesContent":["import {\n Sandbox,\n type AgentProfile,\n type AgentProfileFileMount,\n type AgentProfileMcpServer,\n type SandboxInstance,\n type ScopedTokenScope,\n} from '@tangle-network/sandbox'\nimport {\n buildAppToolMcpServer,\n type AppToolName,\n type AppToolContext,\n type ToolHeaderNames,\n} from '../tools/index'\nimport type { Harness } from '../harness/index'\n\nexport type Outcome<T> =\n | { succeeded: true; value: T }\n | { succeeded: false; error: Error }\n\nconst ok = <T>(value: T): Outcome<T> => ({ succeeded: true, value })\nconst fail = (error: unknown): Outcome<never> => ({\n succeeded: false,\n error: error instanceof Error ? error : new Error(String(error)),\n})\n\nexport interface SandboxClientCredentials {\n apiKey: string\n baseUrl: string\n}\n\nexport interface SandboxResourceConfig {\n image: string\n cpuCores: number\n memoryMB: number\n diskGB: number\n maxLifetimeSeconds: number\n idleTimeoutSeconds: number\n}\n\nexport interface ProviderResolutionConfig {\n routerBaseUrl?: string\n apiKey?: string\n providerName?: string\n modelName?: string\n defaultModel?: string\n openaiApiKey?: string\n}\n\nexport interface SandboxBuildContext {\n workspaceId: string\n connectedIntegrationIds: string[]\n}\n\nexport interface ProfileComposeOptions {\n systemPrompt?: string\n extraFiles?: AgentProfileFileMount[]\n extraMcp?: Record<string, AgentProfileMcpServer>\n name?: string\n}\n\nexport interface SandboxRuntimeConfig {\n credentials: () => SandboxClientCredentials | null\n name: (workspaceId: string) => string\n metadata: (harness: Harness) => Record<string, unknown>\n connectedIntegrationIds: (workspaceId: string) => Promise<string[]>\n env: (ctx: SandboxBuildContext) => Promise<Record<string, string>>\n files: (ctx: SandboxBuildContext) => Promise<AgentProfileFileMount[]>\n secrets: (workspaceId: string) => Promise<string[]>\n profile: (options: ProfileComposeOptions) => AgentProfile\n permissionRole?: (workspaceRole: string) => SandboxPermissionLevel\n resources?: SandboxResourceConfig\n provider?: ProviderResolutionConfig\n}\n\nexport const DEFAULT_SANDBOX_RESOURCES: SandboxResourceConfig = {\n image: 'universal',\n cpuCores: 2,\n memoryMB: 4096,\n diskGB: 10,\n maxLifetimeSeconds: 86400,\n idleTimeoutSeconds: 3600,\n}\n\ninterface ClientCacheEntry {\n client: Sandbox\n fingerprint: string\n}\n\nlet _cached: ClientCacheEntry | null = null\n\nexport function getClient(shell: SandboxRuntimeConfig): Sandbox {\n const creds = shell.credentials()\n if (!creds) throw new Error('sandbox credentials are required (apiKey/baseUrl)')\n\n const fingerprint = `${creds.apiKey} ${creds.baseUrl}`\n if (_cached && _cached.fingerprint === fingerprint) return _cached.client\n\n const client = new Sandbox({ apiKey: creds.apiKey, baseUrl: creds.baseUrl })\n _cached = { client, fingerprint }\n return client\n}\n\nexport function resetClientCache(): void {\n _cached = null\n}\n\nexport interface AppToolDescriptor {\n tool: AppToolName\n key: string\n description: string\n}\n\nexport interface BuildAppToolMcpServersOptions {\n tools: AppToolDescriptor[]\n baseUrl: string\n token: string\n ctx: AppToolContext\n headerNames?: ToolHeaderNames\n}\n\nexport function buildAppToolMcpServers(\n options: BuildAppToolMcpServersOptions,\n): Record<string, AgentProfileMcpServer> {\n const entries: Record<string, AgentProfileMcpServer> = {}\n for (const { tool, key, description } of options.tools) {\n entries[key] = buildAppToolMcpServer({\n tool,\n baseUrl: options.baseUrl,\n token: options.token,\n ctx: options.ctx,\n description,\n headerNames: options.headerNames,\n }) as AgentProfileMcpServer\n }\n return entries\n}\n\nexport interface EnsureWorkspaceSandboxOptions {\n workspaceId: string\n userId?: string\n harness: Harness\n}\n\nasync function listRunning(\n client: Sandbox,\n name: string,\n): Promise<Outcome<SandboxInstance | null>> {\n try {\n const running = await client.list({ status: 'running' })\n return ok(running.find((s) => s.name === name) ?? null)\n } catch (err) {\n return fail(err)\n }\n}\n\nasync function deleteBox(box: SandboxInstance): Promise<Outcome<void>> {\n try {\n await box.delete()\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\n// The SDK narrows `backend.type` to its own BackendType union and\n// `initialUsers[].role` to PermissionLevel — neither symbol is exported. The\n// create payload is assembled with the product's Harness/role strings, which\n// are a superset surface; the localized cast at the boundary is the only place\n// this widening is allowed, and the runtime contract (the sidecar boots the\n// named harness) is what enforces correctness.\ntype CreatePayload = Parameters<Sandbox['create']>[0]\n\nexport async function ensureWorkspaceSandbox(\n shell: SandboxRuntimeConfig,\n options: EnsureWorkspaceSandboxOptions,\n): Promise<SandboxInstance> {\n const { workspaceId, userId, harness } = options\n const client = getClient(shell)\n const name = shell.name(workspaceId)\n const resources = shell.resources ?? DEFAULT_SANDBOX_RESOURCES\n\n const existing = await listRunning(client, name)\n if (existing.succeeded && existing.value) {\n const found = existing.value\n if (found.metadata?.harness === harness) return found\n const dropped = await deleteBox(found)\n if (!dropped.succeeded) {\n throw new Error(\n `harness-mismatched sandbox ${name} ` +\n `(was ${String(found.metadata?.harness ?? 'unknown')}, want ${harness}) could not be deleted`,\n { cause: dropped.error },\n )\n }\n }\n\n const connectedIntegrationIds = await shell.connectedIntegrationIds(workspaceId)\n const buildCtx: SandboxBuildContext = { workspaceId, connectedIntegrationIds }\n const [secrets, env, files] = await Promise.all([\n shell.secrets(workspaceId),\n shell.env(buildCtx),\n shell.files(buildCtx),\n ])\n const profile = shell.profile({ extraFiles: files })\n\n const role = userId && shell.permissionRole ? shell.permissionRole('developer') : undefined\n\n const payload = {\n name,\n image: resources.image,\n metadata: shell.metadata(harness),\n ...(userId ? { permissions: { initialUsers: [{ userId, role }] } } : {}),\n env,\n secrets,\n backend: { type: harness, profile },\n maxLifetimeSeconds: resources.maxLifetimeSeconds,\n idleTimeoutSeconds: resources.idleTimeoutSeconds,\n resources: {\n cpuCores: resources.cpuCores,\n memoryMB: resources.memoryMB,\n diskGB: resources.diskGB,\n },\n } as CreatePayload\n\n const box = await client.create(payload)\n\n await box.waitFor('running', { timeoutMs: 120_000 })\n if (!box.connection?.runtimeUrl) await box.refresh()\n return box\n}\n\nexport interface ResolvedModel {\n model: string\n provider: string\n apiKey: string\n baseUrl?: string\n}\n\nexport function resolveModel(\n config: ProviderResolutionConfig | undefined,\n override?: { model?: string; modelApiKey?: string },\n): ResolvedModel | undefined {\n const c = config ?? {}\n const explicitBaseUrl = c.routerBaseUrl\n const explicitApiKey = override?.modelApiKey ?? c.apiKey\n const provider =\n c.providerName ?? (explicitApiKey ? 'openai-compat' : c.openaiApiKey ? 'openai' : undefined)\n const modelName =\n override?.model ??\n c.modelName ??\n (provider === 'openai' || provider === 'openai-compat' ? c.defaultModel : undefined)\n const apiKey = explicitApiKey ?? (provider === 'openai' ? c.openaiApiKey : undefined)\n if (!provider || !modelName || !apiKey) return undefined\n return {\n model: modelName,\n provider,\n apiKey,\n ...(explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}),\n }\n}\n\nexport function flattenHistory(\n message: string,\n history?: Array<{ role: 'user' | 'assistant'; content: string }>,\n): string {\n if (!history?.length) return message\n const transcript = history\n .map((entry) => `${entry.role === 'assistant' ? 'Assistant' : 'User'}: ${entry.content}`)\n .join('\\n\\n')\n return `${transcript}\\n\\nUser: ${message}`\n}\n\nexport function mergeExtraMcp(\n appToolMcp: Record<string, AgentProfileMcpServer>,\n baseProfileMcp: Record<string, AgentProfileMcpServer>,\n extra: Record<string, AgentProfileMcpServer> | undefined,\n): Record<string, AgentProfileMcpServer> {\n for (const key of Object.keys(extra ?? {})) {\n if (key in appToolMcp || key in baseProfileMcp) {\n throw new Error(`extraMcp key '${key}' collides with an existing profile MCP server`)\n }\n }\n return { ...appToolMcp, ...(extra ?? {}) }\n}\n\nexport function attachReasoningEffort(\n profile: AgentProfile,\n harness: Harness,\n effort: 'auto' | 'low' | 'medium' | 'high' | undefined,\n): AgentProfile {\n if (!effort || effort === 'auto') return profile\n return {\n ...profile,\n extensions: {\n ...(profile.extensions ?? {}),\n [harness]: {\n ...(profile.extensions?.[harness] ?? {}),\n reasoningEffort: effort,\n },\n },\n }\n}\n\nexport interface StreamSandboxPromptOptions {\n sessionId?: string\n executionId?: string\n lastEventId?: string\n systemPrompt?: string\n model?: string\n modelApiKey?: string\n history?: Array<{ role: 'user' | 'assistant'; content: string }>\n harness?: Harness\n effort?: 'auto' | 'low' | 'medium' | 'high'\n appToolMcp?: Record<string, AgentProfileMcpServer>\n baseProfileMcp?: Record<string, AgentProfileMcpServer>\n extraMcp?: Record<string, AgentProfileMcpServer>\n}\n\ntype StreamPromptOptions = Parameters<SandboxInstance['streamPrompt']>[1]\n\nexport async function* streamSandboxPrompt(\n shell: SandboxRuntimeConfig,\n box: SandboxInstance,\n message: string,\n options?: StreamSandboxPromptOptions,\n): AsyncGenerator<unknown> {\n const harness = options?.harness ?? 'opencode'\n const model = resolveModel(shell.provider, {\n model: options?.model,\n modelApiKey: options?.modelApiKey,\n })\n\n const prompt = flattenHistory(message, options?.history)\n\n const appToolMcp = options?.appToolMcp ?? {}\n const extraMcp = mergeExtraMcp(appToolMcp, options?.baseProfileMcp ?? {}, options?.extraMcp)\n\n const profile = shell.profile({ systemPrompt: options?.systemPrompt, extraMcp })\n const profileWithEffort = attachReasoningEffort(profile, harness, options?.effort)\n\n const stream = box.streamPrompt(prompt, {\n sessionId: options?.sessionId,\n executionId: options?.executionId,\n lastEventId: options?.lastEventId,\n backend: {\n type: harness,\n profile: profileWithEffort,\n ...(model ? { model } : {}),\n },\n } as StreamPromptOptions)\n\n for await (const event of stream) yield event\n}\n\nexport async function runSandboxPrompt(\n shell: SandboxRuntimeConfig,\n box: SandboxInstance,\n message: string,\n options?: StreamSandboxPromptOptions,\n): Promise<string> {\n let fullText = ''\n let firstTextSeen = false\n\n for await (const rawEvent of streamSandboxPrompt(shell, box, message, options)) {\n const event = rawEvent as { type?: string; data?: Record<string, unknown> }\n if (!event.type) continue\n\n if (event.type === 'message.part.updated') {\n const part = event.data?.part as Record<string, unknown> | undefined\n const delta = typeof event.data?.delta === 'string' ? event.data.delta : null\n if (String(part?.type ?? '') === 'text') {\n if (!firstTextSeen) {\n firstTextSeen = true\n continue\n }\n if (delta) fullText += delta\n else if (typeof part?.text === 'string') fullText = part.text\n }\n } else if (event.type === 'result') {\n const finalText = typeof event.data?.finalText === 'string' ? event.data.finalText : null\n if (finalText) fullText = finalText\n }\n }\n\n return fullText\n}\n\n// Mirrors the SDK's PermissionLevel union (not re-exported by\n// @tangle-network/sandbox). The product's role-mapping seam must produce one of\n// these; binding the seam's return type to the union makes a wrong mapping a\n// compile error rather than a runtime 400 from the orchestrator.\nexport type SandboxPermissionLevel = 'owner' | 'admin' | 'developer' | 'viewer'\n\nexport interface MemberSyncSeam {\n roleToSandboxRole: (workspaceRole: string) => SandboxPermissionLevel\n}\n\nexport async function syncSandboxMemberAdd(\n box: SandboxInstance,\n seam: MemberSyncSeam,\n userId: string,\n role: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.add({ userId, role: seam.roleToSandboxRole(role) })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function syncSandboxMemberRemove(\n box: SandboxInstance,\n userId: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.remove(userId, { preserveHomeDir: true })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function syncSandboxMemberRole(\n box: SandboxInstance,\n seam: MemberSyncSeam,\n userId: string,\n role: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.update(userId, { role: seam.roleToSandboxRole(role) })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport interface SecretStore {\n create: (name: string, value: string) => Promise<void>\n update: (name: string, value: string) => Promise<void>\n get: (name: string) => Promise<string>\n delete: (name: string) => Promise<void>\n}\n\nexport function secretStoreFromClient(shell: SandboxRuntimeConfig): SecretStore {\n const client = getClient(shell)\n return {\n create: async (name, value) => {\n await client.secrets.create(name, value)\n },\n update: async (name, value) => {\n await client.secrets.update(name, value)\n },\n get: (name) => client.secrets.get(name),\n delete: async (name) => {\n await client.secrets.delete(name)\n },\n }\n}\n\nexport async function storeSecret(\n store: SecretStore,\n name: string,\n value: string,\n): Promise<Outcome<void>> {\n try {\n await store.create(name, value)\n return ok(undefined)\n } catch {\n try {\n await store.update(name, value)\n return ok(undefined)\n } catch (err) {\n return fail(new Error(`Failed to store sandbox secret ${name}`, { cause: err }))\n }\n }\n}\n\nexport async function readSecret(store: SecretStore, name: string): Promise<Outcome<string>> {\n try {\n return ok(await store.get(name))\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function deleteSecret(store: SecretStore, name: string): Promise<Outcome<void>> {\n try {\n await store.delete(name)\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport interface ScopedTokenResult {\n token: string\n expiresAt: Date\n scope: ScopedTokenScope\n}\n\n/**\n * Mint a scoped token for an already-provisioned box (e.g. to hand a terminal\n * proxy a narrowed credential). Uses the SDK's native `box.mintScopedToken`,\n * which normalizes `expiresAt` to a Date — no hand-rolled wire call.\n */\nexport async function mintSandboxScopedToken(\n box: SandboxInstance,\n options: { scope: ScopedTokenScope; sessionId?: string; ttlMinutes?: number },\n): Promise<Outcome<ScopedTokenResult>> {\n try {\n const token = await box.mintScopedToken({\n scope: options.scope,\n ...(options.sessionId ? { sessionId: options.sessionId } : {}),\n ...(options.ttlMinutes ? { ttlMinutes: options.ttlMinutes } : {}),\n })\n return ok({ token: token.token, expiresAt: token.expiresAt, scope: token.scope })\n } catch (err) {\n return fail(err)\n }\n}"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/sandbox/index.ts"],"sourcesContent":["import {\n Sandbox,\n type AgentProfile,\n type AgentProfileFileMount,\n type AgentProfileMcpServer,\n type SandboxInstance,\n type ScopedTokenScope,\n} 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}\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 // 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 backend: {\n type: harness,\n profile: profileWithEffort,\n ...(model ? { model } : {}),\n },\n } as StreamPromptOptions)\n\n for await (const event of stream) yield event\n}\n\nexport async function runSandboxPrompt(\n shell: SandboxRuntimeConfig,\n box: SandboxInstance,\n message: string,\n options?: StreamSandboxPromptOptions,\n): Promise<string> {\n let fullText = ''\n let firstTextSeen = false\n\n for await (const rawEvent of streamSandboxPrompt(shell, box, message, options)) {\n const event = rawEvent as { type?: string; data?: Record<string, unknown> }\n if (!event.type) continue\n\n if (event.type === 'message.part.updated') {\n const part = event.data?.part as Record<string, unknown> | undefined\n const delta = typeof event.data?.delta === 'string' ? event.data.delta : null\n if (String(part?.type ?? '') === 'text') {\n if (!firstTextSeen) {\n firstTextSeen = true\n continue\n }\n if (delta) fullText += delta\n else if (typeof part?.text === 'string') fullText = part.text\n }\n } else if (event.type === 'result') {\n const finalText = typeof event.data?.finalText === 'string' ? event.data.finalText : null\n if (finalText) fullText = finalText\n }\n }\n\n return fullText\n}\n\n// Mirrors the SDK's PermissionLevel union (not re-exported by\n// @tangle-network/sandbox). The product's role-mapping seam must produce one of\n// these; binding the seam's return type to the union makes a wrong mapping a\n// compile error rather than a runtime 400 from the orchestrator.\nexport type SandboxPermissionLevel = 'owner' | 'admin' | 'developer' | 'viewer'\n\nexport interface MemberSyncSeam {\n roleToSandboxRole: (workspaceRole: string) => SandboxPermissionLevel\n}\n\nexport async function syncSandboxMemberAdd(\n box: SandboxInstance,\n seam: MemberSyncSeam,\n userId: string,\n role: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.add({ userId, role: seam.roleToSandboxRole(role) })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function syncSandboxMemberRemove(\n box: SandboxInstance,\n userId: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.remove(userId, { preserveHomeDir: true })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function syncSandboxMemberRole(\n box: SandboxInstance,\n seam: MemberSyncSeam,\n userId: string,\n role: string,\n): Promise<Outcome<void>> {\n try {\n await box.permissions.update(userId, { role: seam.roleToSandboxRole(role) })\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport interface SecretStore {\n create: (name: string, value: string) => Promise<void>\n update: (name: string, value: string) => Promise<void>\n get: (name: string) => Promise<string>\n delete: (name: string) => Promise<void>\n}\n\nexport function secretStoreFromClient(shell: SandboxRuntimeConfig): SecretStore {\n const client = getClient(shell)\n return {\n create: async (name, value) => {\n await client.secrets.create(name, value)\n },\n update: async (name, value) => {\n await client.secrets.update(name, value)\n },\n get: (name) => client.secrets.get(name),\n delete: async (name) => {\n await client.secrets.delete(name)\n },\n }\n}\n\nexport async function storeSecret(\n store: SecretStore,\n name: string,\n value: string,\n): Promise<Outcome<void>> {\n try {\n await store.create(name, value)\n return ok(undefined)\n } catch {\n try {\n await store.update(name, value)\n return ok(undefined)\n } catch (err) {\n return fail(new Error(`Failed to store sandbox secret ${name}`, { cause: err }))\n }\n }\n}\n\nexport async function readSecret(store: SecretStore, name: string): Promise<Outcome<string>> {\n try {\n return ok(await store.get(name))\n } catch (err) {\n return fail(err)\n }\n}\n\nexport async function deleteSecret(store: SecretStore, name: string): Promise<Outcome<void>> {\n try {\n await store.delete(name)\n return ok(undefined)\n } catch (err) {\n return fail(err)\n }\n}\n\nexport interface ScopedTokenResult {\n token: string\n expiresAt: Date\n scope: ScopedTokenScope\n}\n\n/**\n * Mint a scoped token for an already-provisioned box (e.g. to hand a terminal\n * proxy a narrowed credential). Uses the SDK's native `box.mintScopedToken`,\n * which normalizes `expiresAt` to a Date — no hand-rolled wire call.\n */\nexport async function mintSandboxScopedToken(\n box: SandboxInstance,\n options: { scope: ScopedTokenScope; sessionId?: string; ttlMinutes?: number },\n): Promise<Outcome<ScopedTokenResult>> {\n try {\n const token = await box.mintScopedToken({\n scope: options.scope,\n ...(options.sessionId ? { sessionId: options.sessionId } : {}),\n ...(options.ttlMinutes ? { ttlMinutes: options.ttlMinutes } : {}),\n })\n return ok({ token: token.token, expiresAt: token.expiresAt, scope: token.scope })\n } catch (err) {\n return fail(err)\n }\n}"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/agent-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.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":[]}
|