@tangle-network/agent-runtime 0.338.0 → 0.339.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.
Files changed (140) hide show
  1. package/dist/{activation-CsbNax2X.d.ts → activation-CJIIl_Ey.d.ts} +4 -24
  2. package/dist/{activation-kbkAuSmY.js → activation-CVSFxis9.js} +5 -18
  3. package/dist/{activation-kbkAuSmY.js.map → activation-CVSFxis9.js.map} +1 -1
  4. package/dist/agent.d.ts +4 -4
  5. package/dist/agent.js +3 -3
  6. package/dist/{bundle-DM60AI2B.js → bundle-DVOhBTic.js} +2 -2
  7. package/dist/{bundle-DM60AI2B.js.map → bundle-DVOhBTic.js.map} +1 -1
  8. package/dist/candidate-execution/index.d.ts +4 -63
  9. package/dist/candidate-execution/index.js +6 -145
  10. package/dist/candidate-execution/index.js.map +1 -1
  11. package/dist/{delegate-Se1YZKMb.js → delegate-CCWE_AkU.js} +2 -2
  12. package/dist/{delegate-Se1YZKMb.js.map → delegate-CCWE_AkU.js.map} +1 -1
  13. package/dist/{driver-control-acknowledgers-T2a1Ouay.js → driver-control-acknowledgers-C53ggdJ6.js} +12 -8
  14. package/dist/driver-control-acknowledgers-C53ggdJ6.js.map +1 -0
  15. package/dist/{durable-file-B_eJr6Mb.js → durable-file-gNI9ILAn.js} +1 -3
  16. package/dist/durable-file-gNI9ILAn.js.map +1 -0
  17. package/dist/durable.d.ts +3 -35
  18. package/dist/durable.js +10 -10
  19. package/dist/durable.js.map +1 -1
  20. package/dist/{graph-CY0wH838.js → graph-CI9JXKqO.js} +4 -4
  21. package/dist/graph-CI9JXKqO.js.map +1 -0
  22. package/dist/{graph-registry-By0tUj4S.js → graph-registry-BnO64cn8.js} +3 -3
  23. package/dist/{graph-registry-By0tUj4S.js.map → graph-registry-BnO64cn8.js.map} +1 -1
  24. package/dist/{improve-CWGBT13K.js → improve-BOXTLWJ3.js} +8 -35
  25. package/dist/improve-BOXTLWJ3.js.map +1 -0
  26. package/dist/{improvement-adapter-D5gwwoXQ.d.ts → improvement-adapter-HUt_-cWA.d.ts} +2 -20
  27. package/dist/index.d.ts +9 -206
  28. package/dist/index.js +91 -302
  29. package/dist/index.js.map +1 -1
  30. package/dist/intelligence.d.ts +10 -142
  31. package/dist/intelligence.js +7 -81
  32. package/dist/intelligence.js.map +1 -1
  33. package/dist/{jsonl-file-DBv31uPi.js → jsonl-file-C9Kde_0Z.js} +2 -2
  34. package/dist/{jsonl-file-DBv31uPi.js.map → jsonl-file-C9Kde_0Z.js.map} +1 -1
  35. package/dist/kernel.d.ts +4 -3
  36. package/dist/kernel.js +12 -14
  37. package/dist/loop-runner-CWzG1bH4.d.ts +32 -0
  38. package/dist/loop-runner-bin.d.ts +32 -2
  39. package/dist/loop-runner-bin.js +93 -1
  40. package/dist/loop-runner-bin.js.map +1 -0
  41. package/dist/loop-runner-pmPIHFVY.js +59 -0
  42. package/dist/loop-runner-pmPIHFVY.js.map +1 -0
  43. package/dist/mcp/bin.js +3 -3
  44. package/dist/mcp/index.d.ts +99 -418
  45. package/dist/mcp/index.js +121 -203
  46. package/dist/mcp/index.js.map +1 -1
  47. package/dist/mcp/memory-bin.js +231 -1
  48. package/dist/mcp/memory-bin.js.map +1 -1
  49. package/dist/model-policy-CyCUwHed.js.map +1 -1
  50. package/dist/placement.d.ts +2 -9
  51. package/dist/placement.js +1 -1
  52. package/dist/placement.js.map +1 -1
  53. package/dist/platform.d.ts +188 -1
  54. package/dist/platform.js +129 -1
  55. package/dist/platform.js.map +1 -1
  56. package/dist/{prepare-Bt0IouPq.js → prepare-bXkvPMJA.js} +8 -160
  57. package/dist/prepare-bXkvPMJA.js.map +1 -0
  58. package/dist/{profile-Do-5xO_D.js → profile-Dpei_Sq0.js} +2 -2
  59. package/dist/profile-Dpei_Sq0.js.map +1 -0
  60. package/dist/{profile-materialization-BnoEf1co.d.ts → profile-materialization-B1wm2SPT.d.ts} +3 -5
  61. package/dist/profiles.d.ts +2 -2
  62. package/dist/profiles.js +3 -40
  63. package/dist/profiles.js.map +1 -1
  64. package/dist/{protected-model-port-DGxQ8_YM.d.ts → protected-model-port-BGgpy2r9.d.ts} +3 -29
  65. package/dist/protected-redaction-DFCeo4vg.js.map +1 -1
  66. package/dist/{provision-supervisor-0jMufp4h.js → provision-supervisor-CJ6Zz6dz.js} +6 -6
  67. package/dist/{provision-supervisor-0jMufp4h.js.map → provision-supervisor-CJ6Zz6dz.js.map} +1 -1
  68. package/dist/{run-cancellation-BoW5Sg9Y.js → run-cancellation-C9oL62sW.js} +3 -7
  69. package/dist/{run-cancellation-BoW5Sg9Y.js.map → run-cancellation-C9oL62sW.js.map} +1 -1
  70. package/dist/{runtime-D37zRSs7.js → runtime-B2fjL9Oi.js} +2531 -2889
  71. package/dist/runtime-B2fjL9Oi.js.map +1 -0
  72. package/dist/{runtime-hooks-Bj6wJHlH.d.ts → runtime-hooks-nsqxSUFL.d.ts} +2 -6
  73. package/dist/{sandbox-profile-compat-xSoVtdWw.js → sandbox-profile-compat-BzVBFqrz.js} +2 -4
  74. package/dist/sandbox-profile-compat-BzVBFqrz.js.map +1 -0
  75. package/dist/{search-executor-DmgUm5p7.js → search-executor-zMumiak_.js} +3 -3
  76. package/dist/search-executor-zMumiak_.js.map +1 -0
  77. package/dist/{server-DQHJszfk.js → server-_4voDNrl.js} +362 -67
  78. package/dist/server-_4voDNrl.js.map +1 -0
  79. package/dist/{structural-rollout-DAvIE8mw.js → structural-rollout-DpLCXzaL.js} +80 -80
  80. package/dist/structural-rollout-DpLCXzaL.js.map +1 -0
  81. package/dist/{substrate-B0TYNrXn.js → substrate-CGDFTSRh.js} +2 -9
  82. package/dist/substrate-CGDFTSRh.js.map +1 -0
  83. package/dist/{supervise-WuPAwfPh.js → supervise-qAwXmPtD.js} +10 -57
  84. package/dist/supervise-qAwXmPtD.js.map +1 -0
  85. package/dist/{supervisor-CPG1I9UN.js → supervisor-DHrUJhEZ.js} +179 -236
  86. package/dist/supervisor-DHrUJhEZ.js.map +1 -0
  87. package/dist/testing.d.ts +2 -2
  88. package/dist/testing.js +13 -13
  89. package/dist/testing.js.map +1 -1
  90. package/dist/tool-loop.d.ts +1 -1
  91. package/dist/{tool-server-Dqer2M4_.js → tool-server-XV4aJwoQ.js} +2 -2
  92. package/dist/tool-server-XV4aJwoQ.js.map +1 -0
  93. package/dist/top-snapshot-CbZE1JsU.js +502 -0
  94. package/dist/top-snapshot-CbZE1JsU.js.map +1 -0
  95. package/dist/tui/bin.js +711 -1
  96. package/dist/tui/bin.js.map +1 -1
  97. package/dist/tui/index.d.ts +2 -70
  98. package/dist/tui/index.js +3 -3
  99. package/dist/{types-CrNWif8-.d.ts → types-CaOXqRTq.d.ts} +713 -2096
  100. package/dist/workspace-store/index.d.ts +2 -27
  101. package/dist/workspace-store/index.js +2 -2
  102. package/dist/workspace-store/index.js.map +1 -1
  103. package/dist/{workspace-streams-BZZds1Fw.d.ts → workspace-streams-BiS4ABah.d.ts} +3 -18
  104. package/package.json +1 -1
  105. package/skills/agent-graphs/references/authoring.md +3 -2
  106. package/dist/coder-yhVWbdWc.js +0 -40
  107. package/dist/coder-yhVWbdWc.js.map +0 -1
  108. package/dist/delegation-status-CF4D0NZ_.js +0 -358
  109. package/dist/delegation-status-CF4D0NZ_.js.map +0 -1
  110. package/dist/driver-control-acknowledgers-T2a1Ouay.js.map +0 -1
  111. package/dist/durable-file-B_eJr6Mb.js.map +0 -1
  112. package/dist/graph-CY0wH838.js.map +0 -1
  113. package/dist/improve-CWGBT13K.js.map +0 -1
  114. package/dist/integrations-6xttbPi9.d.ts +0 -190
  115. package/dist/integrations-dFi-ALk7.js +0 -132
  116. package/dist/integrations-dFi-ALk7.js.map +0 -1
  117. package/dist/kb-gate-C8z2juK8.d.ts +0 -80
  118. package/dist/kb-gate-DpaSwXVx.js +0 -80
  119. package/dist/kb-gate-DpaSwXVx.js.map +0 -1
  120. package/dist/loop-runner-bin-CFnrAHpl.d.ts +0 -164
  121. package/dist/loop-runner-bin-CdPlhy9g.js +0 -246
  122. package/dist/loop-runner-bin-CdPlhy9g.js.map +0 -1
  123. package/dist/memory-server-DXQ8IxGR.js +0 -234
  124. package/dist/memory-server-DXQ8IxGR.js.map +0 -1
  125. package/dist/openai-tools-DoQ32vpe.js +0 -44
  126. package/dist/openai-tools-DoQ32vpe.js.map +0 -1
  127. package/dist/openai-tools-zh2I_uD7.d.ts +0 -24
  128. package/dist/prepare-Bt0IouPq.js.map +0 -1
  129. package/dist/profile-Do-5xO_D.js.map +0 -1
  130. package/dist/runtime-D37zRSs7.js.map +0 -1
  131. package/dist/sandbox-profile-compat-xSoVtdWw.js.map +0 -1
  132. package/dist/search-executor-DmgUm5p7.js.map +0 -1
  133. package/dist/server-DQHJszfk.js.map +0 -1
  134. package/dist/structural-rollout-DAvIE8mw.js.map +0 -1
  135. package/dist/substrate-B0TYNrXn.js.map +0 -1
  136. package/dist/supervise-WuPAwfPh.js.map +0 -1
  137. package/dist/supervisor-CPG1I9UN.js.map +0 -1
  138. package/dist/tool-server-Dqer2M4_.js.map +0 -1
  139. package/dist/top-app-C55Wa7uy.js +0 -1214
  140. package/dist/top-app-C55Wa7uy.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"placement.js","names":[],"sources":["../src/placement/index.ts"],"sourcesContent":["/**\n * Execution placement: choose the cheapest executor whose declared capabilities\n * cover the work, before any key, sandbox or model call.\n *\n * A pure decision over DECLARED data — the work's requirements and its resolved\n * {@link AgentProfile}. It never reads the prompt and does no I/O, so it is safe\n * in a Worker or Durable Object and costs nothing on the path to the first word.\n * Tiers, cheapest first:\n * - `inference`: one Router call, when nothing needs a tool, a file or a session;\n * - `loop`: the Runtime tool loop in the caller's own process (a Worker or the\n * Platform workflow worker), when the only tools are ones that loop holds;\n * - `sandbox`: anything that needs a harness, a filesystem, a shell, files, MCP\n * servers, subagents, hooks or an interactive session.\n * A requirement no tier can meet refuses the work by name\n * ({@link PlacementRefusedError}); a capability is never dropped silently, and a\n * pinned harness always runs in the sandbox.\n *\n * This is Platform's `agent-placement/2` (`selectAgentPlacement` and\n * `workerProfileRefusal`) moved here unchanged in semantics. Its reason strings\n * are kept byte-identical so a shadow comparison against Platform's copy reads\n * equal; only the tier name changes (`worker` → `loop`). `agent-placement/3`\n * adds two caller opt-ins, both off by default:\n * - {@link PlaceExecutionInput.vacuousAxes}: a deny-only `permissions` map asks\n * for nothing a tier lacks, so it counts as covered;\n * - {@link PlaceExecutionInput.inlineSkills}: profile skills count as covered on\n * `loop` when the caller inlines their content.\n */\n\nimport type { AgentProfile } from '@tangle-network/agent-interface'\nimport type {\n AgentProfileMaterializationAxis,\n ProfileMaterializationContract,\n} from '../agent/profile-materialization'\n\n/** The policy version every {@link PlacementRecord} carries. */\nexport const PLACEMENT_POLICY = 'agent-placement/3'\n\n/** Where one unit of agent work runs, cheapest first. */\nexport type ExecutionTier = 'inference' | 'loop' | 'sandbox'\n\n/** What was chosen, under which policy, why, and why each other tier was refused. */\nexport interface PlacementRecord {\n chosen: ExecutionTier\n policy: typeof PLACEMENT_POLICY\n reason: string\n /**\n * The profile's declared materialization axes the chosen tier carries. A tier that\n * cannot carry a declared axis is rejected, so on a fresh decision this is every\n * declared axis; an authored or resumed record makes no coverage decision and is empty.\n */\n coveredAxes: AgentProfileMaterializationAxis[]\n rejected: Array<{ placement: ExecutionTier; reason: string }>\n}\n\n/**\n * The declared requirements of the work, beside its profile. Field names follow\n * Platform's `agent.run` action, the first consumer; an app's chat turn usually\n * declares none of them.\n */\nexport interface PlacementRequirements {\n /** A pinned harness. Always runs in the sandbox. */\n harness?: string\n /** A model other than the profile's own needs the sandbox to choose between them. */\n model?: string\n source?: unknown\n /** Skill ids resolved into a box. Profile skills are `profile.resources.skills`. */\n skills?: unknown\n env?: unknown\n image?: unknown\n size?: unknown\n resources?: unknown\n fromSnapshot?: unknown\n fromSandboxId?: unknown\n resume?: unknown\n maxRounds?: number\n ask?: boolean\n /** Hub connections: tools the loop holds. */\n connections?: readonly string[]\n /** Owner-object grants: tools only the loop holds. */\n objects?: unknown\n /** Public HTTPS reads: a tool only the loop holds. */\n http?: unknown\n /** Tools the caller binds into the loop itself, such as an app's product tools. */\n tools?: readonly string[]\n}\n\n/** Which tiers this deployment has wired. */\nexport interface PlacementContext {\n interactive: boolean\n inferenceAvailable: boolean\n loopAvailable: boolean\n /** Why the loop cannot take this work, when it cannot. */\n loopUnavailableReason?: string\n}\n\n/** Profile axes a tier may count as covered when their declared value grants nothing. */\nexport type VacuousAxis = 'permissions'\n\nexport interface PlaceExecutionInput {\n requirements?: PlacementRequirements\n /** The RESOLVED profile: a named profile is read before the decision. */\n profile?: AgentProfile | object\n context: PlacementContext\n /**\n * Axes whose deny-only value counts as covered on `inference` and `loop`.\n * `permissions` qualifies when every rule is `deny`: a tier without native tools\n * already denies them. Deny-only `tools` are covered without opting in.\n */\n vacuousAxes?: readonly VacuousAxis[]\n /**\n * The caller inlines the profile's skill content into the loop's context, so\n * `resources.skills` (with a strict `resources.failOnError`) and the native `skill`\n * tool and permission that load them count as covered on `loop`. Off by default:\n * the skills then keep the work in the sandbox.\n */\n inlineSkills?: boolean\n}\n\n/** No tier can meet every declared requirement. Raised before any spend. */\nexport class PlacementRefusedError extends Error {\n constructor(readonly reason: string) {\n super(`placement cannot run this work — ${reason}`)\n this.name = 'PlacementRefusedError'\n }\n}\n\n/**\n * The profile axes `inference` and `loop` carry: identity, prompt, model controls and\n * metadata. No harness, native tools, files, MCP, subagents, hooks or modes.\n */\nexport const loopTierProfileMaterialization: ProfileMaterializationContract = {\n name: 'placement-loop-tier',\n axes: [\n 'name',\n 'description',\n 'version',\n 'tags',\n 'systemPrompt',\n 'appendSystemPrompt',\n 'instructions',\n 'modelDefault',\n 'modelSmall',\n 'modelProvider',\n 'modelReasoningEffort',\n 'modelMaxVisibleOutputTokens',\n 'modelMaxTotalOutputTokens',\n 'modelMetadata',\n 'metadata',\n ],\n}\n\n/** Compound profile properties and the axis each child maps to. */\nconst COMPOUND_FIELDS: Record<string, Record<string, AgentProfileMaterializationAxis>> = {\n prompt: {\n systemPrompt: 'systemPrompt',\n appendSystemPrompt: 'appendSystemPrompt',\n instructions: 'instructions',\n },\n model: {\n default: 'modelDefault',\n small: 'modelSmall',\n provider: 'modelProvider',\n reasoningEffort: 'modelReasoningEffort',\n maxVisibleOutputTokens: 'modelMaxVisibleOutputTokens',\n maxReasoningTokens: 'modelMaxReasoningTokens',\n maxTotalOutputTokens: 'modelMaxTotalOutputTokens',\n metadata: 'modelMetadata',\n },\n resources: {\n files: 'files',\n tools: 'resourceTools',\n skills: 'skills',\n agents: 'resourceAgents',\n commands: 'commands',\n instructions: 'resourceInstructions',\n failOnError: 'resourceFailOnError',\n },\n}\n\n/** Top-level profile properties that are themselves an axis (the rest are compounds). */\nconst TOP_LEVEL_AXES: ReadonlySet<string> = new Set([\n 'name',\n 'description',\n 'version',\n 'tags',\n 'harness',\n 'seats',\n 'permissions',\n 'tools',\n 'mcp',\n 'connections',\n 'subagents',\n 'hooks',\n 'modes',\n 'confidential',\n 'metadata',\n 'extensions',\n])\n\n/** Options for {@link loopProfileRefusal}. */\nexport interface LoopProfileRefusalOptions {\n vacuousAxes?: readonly VacuousAxis[]\n inlineSkills?: boolean\n}\n\n/**\n * Why `inference` and `loop` cannot run this profile, or undefined when they can.\n * A profile that needs a harness, files, harness built-in tools, MCP servers,\n * subagents, hooks or modes needs a sandbox; the loop never drops one silently.\n * Platform's `workerProfileRefusal`, read through {@link loopTierProfileMaterialization}.\n */\nexport function loopProfileRefusal(\n profile: object,\n options: LoopProfileRefusalOptions = {},\n): string | undefined {\n const fields = profile as Record<string, unknown>\n const vacuousPermissions = options.vacuousAxes?.includes('permissions') === true\n const inlineSkills = options.inlineSkills === true\n const carried = new Set<string>(loopTierProfileMaterialization.axes)\n if (inlineSkills) carried.add('skills')\n const childCovered = (field: string, child: string, value: unknown): boolean => {\n const axis = COMPOUND_FIELDS[field]?.[child]\n if (axis === undefined) return false\n // A strict failure policy is honoured by an inliner that fails closed; a best-effort\n // one would have to report a skipped skill, which the loop has no channel for.\n if (axis === 'resourceFailOnError') return inlineSkills && value === true\n return carried.has(axis)\n }\n const reasons: string[] = []\n for (const [field, value] of Object.entries(fields)) {\n if (value === undefined) continue\n if (field === 'tools') continue\n if (\n field === 'permissions' &&\n vacuousPermissions &&\n permissionsGrantNothing(value, inlineSkills)\n )\n continue\n if (TOP_LEVEL_AXES.has(field) && carried.has(field)) continue\n const children = COMPOUND_FIELDS[field]\n if (children && Object.values(children).some((axis) => carried.has(axis))) continue\n reasons.push(`profile ${field} needs a sandbox`)\n }\n for (const field of Object.keys(COMPOUND_FIELDS)) {\n const value = fields[field]\n if (typeof value !== 'object' || value === null) continue\n // A compound the loop carries none of was refused whole above.\n if (!Object.values(COMPOUND_FIELDS[field] ?? {}).some((axis) => carried.has(axis))) continue\n for (const [child, inner] of Object.entries(value)) {\n if (inner !== undefined && !childCovered(field, child, inner)) {\n reasons.push(`profile ${field}.${child} is not supported by the worker`)\n }\n }\n }\n // Harness built-in tools (shell, edit, browser) exist only in a sandbox. A profile may\n // still switch them off. The `skill` tool only loads skills, which an inliner supplies.\n const tools = fields.tools\n if (typeof tools === 'object' && tools !== null) {\n const enabled = Object.entries(tools)\n .filter(([name, on]) => on === true && !(inlineSkills && name === 'skill'))\n .map(([name]) => name)\n if (enabled.length > 0) reasons.push(`profile tools ${enabled.join(', ')} need a sandbox`)\n }\n return reasons.length > 0 ? reasons.join('; ') : undefined\n}\n\n/** True when every permission rule denies, or allows only `skill` under inlined skills. */\nfunction permissionsGrantNothing(value: unknown, inlineSkills: boolean): boolean {\n if (typeof value !== 'object' || value === null) return false\n return Object.entries(value).every(([tool, rule]) => {\n if (inlineSkills && tool === 'skill') return true\n if (rule === 'deny') return true\n if (typeof rule !== 'object' || rule === null) return false\n return Object.values(rule).every((inner) => inner === 'deny')\n })\n}\n\n/** Requirement fields that only mean something inside a provisioned box. */\nconst SANDBOX_FIELDS = [\n 'source',\n 'skills',\n 'env',\n 'image',\n 'size',\n 'resources',\n 'fromSnapshot',\n 'fromSandboxId',\n 'resume',\n] as const satisfies ReadonlyArray<keyof PlacementRequirements>\n\ninterface RequirementOptions extends LoopProfileRefusalOptions {\n requirements: PlacementRequirements\n profile?: object\n interactive: boolean\n}\n\n/** Every declared requirement only a sandbox can meet, in field order. */\nfunction sandboxRequirements(options: RequirementOptions): string[] {\n const { requirements: cfg, profile } = options\n const needs: string[] = []\n if (options.interactive) needs.push('interactive run needs a session')\n if (cfg.harness !== undefined) needs.push(`harness ${cfg.harness} is pinned`)\n for (const field of SANDBOX_FIELDS) {\n if (cfg[field] !== undefined) needs.push(`${field} needs a sandbox`)\n }\n if (cfg.maxRounds !== undefined && cfg.maxRounds > 1)\n needs.push('maxRounds > 1 needs a multi-round session')\n if (cfg.ask === true) needs.push('ask needs a session to answer into')\n const refusal = profile ? loopProfileRefusal(profile, options) : undefined\n if (refusal) needs.push(refusal)\n // Inference and the loop run the profile's own model and refuse another; only the\n // sandbox decides between a requested model and the profile's.\n const profileModel = profileDefaultModel(profile)\n if (cfg.model !== undefined && profileModel !== undefined && cfg.model !== profileModel) {\n needs.push(\n `the action model ${cfg.model} differs from the profile model ${String(profileModel)}`,\n )\n }\n return needs\n}\n\nfunction profileDefaultModel(profile: object | undefined): unknown {\n return (profile as { model?: { default?: unknown } } | undefined)?.model?.default\n}\n\n/** Options for {@link sandboxOnlyRequirements}. */\nexport interface SandboxOnlyRequirementsOptions extends LoopProfileRefusalOptions {\n requirements?: PlacementRequirements\n profile?: AgentProfile | object\n interactive: boolean\n}\n\n/**\n * Every declared requirement only a sandbox can meet. A caller resuming work in the\n * loop it began in checks this is empty before it continues there.\n */\nexport function sandboxOnlyRequirements(options: SandboxOnlyRequirementsOptions): string[] {\n return sandboxRequirements({ ...options, requirements: options.requirements ?? {} })\n}\n\n/** Tools the work grants, each of which the loop can hold. */\nfunction toolRequirements(cfg: PlacementRequirements): string[] {\n const tools: string[] = []\n if ((cfg.connections ?? []).length > 0) tools.push('connections need Hub tools')\n if (cfg.objects !== undefined) tools.push('objects need object tools')\n if (cfg.http !== undefined) tools.push('http needs a fetch tool')\n if ((cfg.tools ?? []).length > 0) tools.push(`tools ${cfg.tools?.join(', ')} need the loop`)\n return tools\n}\n\n/** Grants only the loop holds: a sandbox cannot honour them. */\nfunction loopOnlyGrants(cfg: PlacementRequirements): string[] {\n return [\n ...(cfg.objects !== undefined ? ['objects'] : []),\n ...(cfg.http !== undefined ? ['http'] : []),\n ]\n}\n\n/** True when the profile declares skills that only an inliner on the loop can carry. */\nfunction declaresProfileSkills(profile: object | undefined): boolean {\n const fields = (profile ?? {}) as {\n resources?: { skills?: unknown }\n tools?: Record<string, unknown>\n permissions?: Record<string, unknown>\n }\n const skills = fields.resources?.skills\n return (\n (Array.isArray(skills) && skills.length > 0) ||\n fields.tools?.skill === true ||\n (fields.permissions?.skill !== undefined && fields.permissions.skill !== 'deny')\n )\n}\n\n/**\n * Choose where work runs: the cheapest tier every declared requirement allows.\n * Throws {@link PlacementRefusedError} when no tier can meet every requirement.\n */\nexport function placeExecution(input: PlaceExecutionInput): PlacementRecord {\n const cfg = input.requirements ?? {}\n const { context, profile } = input\n const opts: LoopProfileRefusalOptions = {\n ...(input.vacuousAxes ? { vacuousAxes: input.vacuousAxes } : {}),\n ...(input.inlineSkills ? { inlineSkills: true } : {}),\n }\n const sandboxNeeds = sandboxRequirements({\n requirements: cfg,\n profile,\n interactive: context.interactive,\n ...opts,\n })\n const tools = toolRequirements(cfg)\n if (input.inlineSkills && declaresProfileSkills(profile))\n tools.push('skills are inlined by the loop')\n const loopOnly = loopOnlyGrants(cfg)\n const coveredAxes = profile ? declaredAxes(profile) : []\n const record = (\n chosen: ExecutionTier,\n reason: string,\n rejected: PlacementRecord['rejected'],\n ): PlacementRecord => ({ chosen, policy: PLACEMENT_POLICY, reason, coveredAxes, rejected })\n\n if (sandboxNeeds.length > 0) {\n const reason = sandboxNeeds.join('; ')\n if (loopOnly.length > 0) {\n throw new PlacementRefusedError(\n `${loopOnly.join(' and ')} run only in the worker, but ${reason}`,\n )\n }\n return record('sandbox', reason, [\n { placement: 'inference', reason },\n { placement: 'loop', reason },\n ])\n }\n\n // Past the sandbox check the profile is one the loop (and a single Router call) can\n // honour, so only granted tools separate the two.\n if (tools.length > 0) {\n const reason = `${tools.join('; ')}, which the worker holds without a sandbox`\n if (!context.loopAvailable) {\n const unavailable =\n context.loopUnavailableReason ?? 'worker placement is not wired on this deployment'\n if (loopOnly.length > 0) {\n throw new PlacementRefusedError(\n `${loopOnly.join(' and ')} need the worker, but ${unavailable}`,\n )\n }\n // Inlined skills are the one loop need a sandbox also meets, natively.\n return record('sandbox', unavailable, [\n { placement: 'inference', reason: tools.join('; ') },\n { placement: 'loop', reason: unavailable },\n ])\n }\n return record('loop', reason, [\n { placement: 'inference', reason: tools.join('; ') },\n { placement: 'sandbox', reason: 'costs more and no declared requirement needs it' },\n ])\n }\n\n if (context.inferenceAvailable) {\n return record('inference', 'no declared requirement needs tools, files or a session', [\n { placement: 'loop', reason: 'no declared tool needs a loop' },\n { placement: 'sandbox', reason: 'costs more and no declared requirement needs it' },\n ])\n }\n const unwired = 'inference is not wired on this deployment'\n if (context.loopAvailable) {\n return record('loop', `${unwired}; one tool-free worker turn`, [\n { placement: 'inference', reason: unwired },\n { placement: 'sandbox', reason: 'costs more and no declared requirement needs it' },\n ])\n }\n return record('sandbox', unwired, [\n { placement: 'inference', reason: unwired },\n { placement: 'loop', reason: 'worker placement is not wired on this deployment' },\n ])\n}\n\n/** The materialization axes a profile declares, in its own field order. Read from the\n * same field map as {@link loopProfileRefusal}, so the module has no runtime import. */\nfunction declaredAxes(profile: object): AgentProfileMaterializationAxis[] {\n const axes: AgentProfileMaterializationAxis[] = []\n for (const [field, value] of Object.entries(profile)) {\n if (value === undefined) continue\n if (TOP_LEVEL_AXES.has(field)) axes.push(field as AgentProfileMaterializationAxis)\n const children = COMPOUND_FIELDS[field]\n if (!children || typeof value !== 'object' || value === null) continue\n for (const [child, inner] of Object.entries(value)) {\n const axis = children[child]\n if (axis !== undefined && inner !== undefined) axes.push(axis)\n }\n }\n return axes\n}\n\n/** The record for a tier the author named: nothing was chosen or refused. */\nexport function authoredPlacement(chosen: ExecutionTier): PlacementRecord {\n return {\n chosen,\n policy: PLACEMENT_POLICY,\n reason: `authored placement: ${chosen}`,\n coveredAxes: [],\n rejected: [],\n }\n}\n\n/**\n * The record for work that resumes under the tier it began with. A retry reuses the\n * recorded tier and never re-decides, so one turn never runs on two tiers; a caller\n * resuming on `loop` first checks {@link sandboxOnlyRequirements} is empty.\n */\nexport function resumedPlacement(chosen: ExecutionTier): PlacementRecord {\n return {\n chosen,\n policy: PLACEMENT_POLICY,\n reason: `resumed under the placement it began with: ${chosen}`,\n coveredAxes: [],\n rejected: [],\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"placement.js","names":[],"sources":["../src/placement/index.ts"],"sourcesContent":["/**\n * Execution placement: choose the cheapest executor whose declared capabilities\n * cover the work, before any key, sandbox or model call.\n *\n * A pure decision over DECLARED data — the work's requirements and its resolved\n * {@link AgentProfile}. It never reads the prompt and does no I/O, so it is safe\n * in a Worker or Durable Object and costs nothing on the path to the first word.\n * Tiers, cheapest first:\n * - `inference`: one Router call, when nothing needs a tool, a file or a session;\n * - `loop`: the Runtime tool loop in the caller's own process (a Worker or the\n * Platform workflow worker), when the only tools are ones that loop holds;\n * - `sandbox`: anything that needs a harness, a filesystem, a shell, files, MCP\n * servers, subagents, hooks or an interactive session.\n * A requirement no tier can meet refuses the work by name\n * ({@link PlacementRefusedError}); a capability is never dropped silently, and a\n * pinned harness always runs in the sandbox.\n *\n * This is Platform's `agent-placement/2` (`selectAgentPlacement` and\n * `workerProfileRefusal`) moved here unchanged in semantics. Its reason strings\n * are kept byte-identical so a shadow comparison against Platform's copy reads\n * equal; only the tier name changes (`worker` → `loop`). `agent-placement/3`\n * adds two caller opt-ins, both off by default:\n * - {@link PlaceExecutionInput.vacuousAxes}: a deny-only `permissions` map asks\n * for nothing a tier lacks, so it counts as covered;\n * - {@link PlaceExecutionInput.inlineSkills}: profile skills count as covered on\n * `loop` when the caller inlines their content.\n */\n\nimport type { AgentProfile } from '@tangle-network/agent-interface'\nimport type {\n AgentProfileMaterializationAxis,\n ProfileMaterializationContract,\n} from '../agent/profile-materialization'\n\n/** The policy version every {@link PlacementRecord} carries. */\nexport const PLACEMENT_POLICY = 'agent-placement/3'\n\n/** Where one unit of agent work runs, cheapest first. */\nexport type ExecutionTier = 'inference' | 'loop' | 'sandbox'\n\n/** What was chosen, under which policy, why, and why each other tier was refused. */\nexport interface PlacementRecord {\n chosen: ExecutionTier\n policy: typeof PLACEMENT_POLICY\n reason: string\n /**\n * The profile's declared materialization axes the chosen tier carries. A tier that\n * cannot carry a declared axis is rejected, so on a fresh decision this is every\n * declared axis; an authored or resumed record makes no coverage decision and is empty.\n */\n coveredAxes: AgentProfileMaterializationAxis[]\n rejected: Array<{ placement: ExecutionTier; reason: string }>\n}\n\n/**\n * The declared requirements of the work, beside its profile. Field names follow\n * Platform's `agent.run` action, the first consumer; an app's chat turn usually\n * declares none of them.\n */\nexport interface PlacementRequirements {\n /** A pinned harness. Always runs in the sandbox. */\n harness?: string\n /** A model other than the profile's own needs the sandbox to choose between them. */\n model?: string\n source?: unknown\n /** Skill ids resolved into a box. Profile skills are `profile.resources.skills`. */\n skills?: unknown\n env?: unknown\n image?: unknown\n size?: unknown\n resources?: unknown\n fromSnapshot?: unknown\n fromSandboxId?: unknown\n resume?: unknown\n maxRounds?: number\n ask?: boolean\n /** Hub connections: tools the loop holds. */\n connections?: readonly string[]\n /** Owner-object grants: tools only the loop holds. */\n objects?: unknown\n /** Public HTTPS reads: a tool only the loop holds. */\n http?: unknown\n /** Tools the caller binds into the loop itself, such as an app's product tools. */\n tools?: readonly string[]\n}\n\n/** Which tiers this deployment has wired. */\nexport interface PlacementContext {\n interactive: boolean\n inferenceAvailable: boolean\n loopAvailable: boolean\n /** Why the loop cannot take this work, when it cannot. */\n loopUnavailableReason?: string\n}\n\n/** Profile axes a tier may count as covered when their declared value grants nothing. */\nexport type VacuousAxis = 'permissions'\n\nexport interface PlaceExecutionInput {\n requirements?: PlacementRequirements\n /** The RESOLVED profile: a named profile is read before the decision. */\n profile?: AgentProfile | object\n context: PlacementContext\n /**\n * Axes whose deny-only value counts as covered on `inference` and `loop`.\n * `permissions` qualifies when every rule is `deny`: a tier without native tools\n * already denies them. Deny-only `tools` are covered without opting in.\n */\n vacuousAxes?: readonly VacuousAxis[]\n /**\n * The caller inlines the profile's skill content into the loop's context, so\n * `resources.skills` (with a strict `resources.failOnError`) and the native `skill`\n * tool and permission that load them count as covered on `loop`. Off by default:\n * the skills then keep the work in the sandbox.\n */\n inlineSkills?: boolean\n}\n\n/** No tier can meet every declared requirement. Raised before any spend. */\nexport class PlacementRefusedError extends Error {\n constructor(readonly reason: string) {\n super(`placement cannot run this work — ${reason}`)\n this.name = 'PlacementRefusedError'\n }\n}\n\n/**\n * The profile axes `inference` and `loop` carry: identity, prompt, model controls and\n * metadata. No harness, native tools, files, MCP, subagents, hooks or modes.\n */\nexport const loopTierProfileMaterialization: ProfileMaterializationContract = {\n name: 'placement-loop-tier',\n axes: [\n 'name',\n 'description',\n 'version',\n 'tags',\n 'systemPrompt',\n 'appendSystemPrompt',\n 'instructions',\n 'modelDefault',\n 'modelSmall',\n 'modelProvider',\n 'modelReasoningEffort',\n 'modelMaxVisibleOutputTokens',\n 'modelMaxTotalOutputTokens',\n 'modelMetadata',\n 'metadata',\n ],\n}\n\n/** Compound profile properties and the axis each child maps to. */\nconst COMPOUND_FIELDS: Record<string, Record<string, AgentProfileMaterializationAxis>> = {\n prompt: {\n systemPrompt: 'systemPrompt',\n appendSystemPrompt: 'appendSystemPrompt',\n instructions: 'instructions',\n },\n model: {\n default: 'modelDefault',\n small: 'modelSmall',\n provider: 'modelProvider',\n reasoningEffort: 'modelReasoningEffort',\n maxVisibleOutputTokens: 'modelMaxVisibleOutputTokens',\n maxReasoningTokens: 'modelMaxReasoningTokens',\n maxTotalOutputTokens: 'modelMaxTotalOutputTokens',\n metadata: 'modelMetadata',\n },\n resources: {\n files: 'files',\n tools: 'resourceTools',\n skills: 'skills',\n agents: 'resourceAgents',\n commands: 'commands',\n instructions: 'resourceInstructions',\n failOnError: 'resourceFailOnError',\n },\n}\n\n/** Top-level profile properties that are themselves an axis (the rest are compounds). */\nconst TOP_LEVEL_AXES: ReadonlySet<string> = new Set([\n 'name',\n 'description',\n 'version',\n 'tags',\n 'harness',\n 'seats',\n 'permissions',\n 'tools',\n 'mcp',\n 'connections',\n 'subagents',\n 'hooks',\n 'modes',\n 'confidential',\n 'metadata',\n 'extensions',\n])\n\n/** Options for {@link loopProfileRefusal}. */\ninterface LoopProfileRefusalOptions {\n vacuousAxes?: readonly VacuousAxis[]\n inlineSkills?: boolean\n}\n\n/**\n * Why `inference` and `loop` cannot run this profile, or undefined when they can.\n * A profile that needs a harness, files, harness built-in tools, MCP servers,\n * subagents, hooks or modes needs a sandbox; the loop never drops one silently.\n * Platform's `workerProfileRefusal`, read through {@link loopTierProfileMaterialization}.\n */\nfunction loopProfileRefusal(\n profile: object,\n options: LoopProfileRefusalOptions = {},\n): string | undefined {\n const fields = profile as Record<string, unknown>\n const vacuousPermissions = options.vacuousAxes?.includes('permissions') === true\n const inlineSkills = options.inlineSkills === true\n const carried = new Set<string>(loopTierProfileMaterialization.axes)\n if (inlineSkills) carried.add('skills')\n const childCovered = (field: string, child: string, value: unknown): boolean => {\n const axis = COMPOUND_FIELDS[field]?.[child]\n if (axis === undefined) return false\n // A strict failure policy is honoured by an inliner that fails closed; a best-effort\n // one would have to report a skipped skill, which the loop has no channel for.\n if (axis === 'resourceFailOnError') return inlineSkills && value === true\n return carried.has(axis)\n }\n const reasons: string[] = []\n for (const [field, value] of Object.entries(fields)) {\n if (value === undefined) continue\n if (field === 'tools') continue\n if (\n field === 'permissions' &&\n vacuousPermissions &&\n permissionsGrantNothing(value, inlineSkills)\n )\n continue\n if (TOP_LEVEL_AXES.has(field) && carried.has(field)) continue\n const children = COMPOUND_FIELDS[field]\n if (children && Object.values(children).some((axis) => carried.has(axis))) continue\n reasons.push(`profile ${field} needs a sandbox`)\n }\n for (const field of Object.keys(COMPOUND_FIELDS)) {\n const value = fields[field]\n if (typeof value !== 'object' || value === null) continue\n // A compound the loop carries none of was refused whole above.\n if (!Object.values(COMPOUND_FIELDS[field] ?? {}).some((axis) => carried.has(axis))) continue\n for (const [child, inner] of Object.entries(value)) {\n if (inner !== undefined && !childCovered(field, child, inner)) {\n reasons.push(`profile ${field}.${child} is not supported by the worker`)\n }\n }\n }\n // Harness built-in tools (shell, edit, browser) exist only in a sandbox. A profile may\n // still switch them off. The `skill` tool only loads skills, which an inliner supplies.\n const tools = fields.tools\n if (typeof tools === 'object' && tools !== null) {\n const enabled = Object.entries(tools)\n .filter(([name, on]) => on === true && !(inlineSkills && name === 'skill'))\n .map(([name]) => name)\n if (enabled.length > 0) reasons.push(`profile tools ${enabled.join(', ')} need a sandbox`)\n }\n return reasons.length > 0 ? reasons.join('; ') : undefined\n}\n\n/** True when every permission rule denies, or allows only `skill` under inlined skills. */\nfunction permissionsGrantNothing(value: unknown, inlineSkills: boolean): boolean {\n if (typeof value !== 'object' || value === null) return false\n return Object.entries(value).every(([tool, rule]) => {\n if (inlineSkills && tool === 'skill') return true\n if (rule === 'deny') return true\n if (typeof rule !== 'object' || rule === null) return false\n return Object.values(rule).every((inner) => inner === 'deny')\n })\n}\n\n/** Requirement fields that only mean something inside a provisioned box. */\nconst SANDBOX_FIELDS = [\n 'source',\n 'skills',\n 'env',\n 'image',\n 'size',\n 'resources',\n 'fromSnapshot',\n 'fromSandboxId',\n 'resume',\n] as const satisfies ReadonlyArray<keyof PlacementRequirements>\n\ninterface RequirementOptions extends LoopProfileRefusalOptions {\n requirements: PlacementRequirements\n profile?: object\n interactive: boolean\n}\n\n/** Every declared requirement only a sandbox can meet, in field order. */\nfunction sandboxRequirements(options: RequirementOptions): string[] {\n const { requirements: cfg, profile } = options\n const needs: string[] = []\n if (options.interactive) needs.push('interactive run needs a session')\n if (cfg.harness !== undefined) needs.push(`harness ${cfg.harness} is pinned`)\n for (const field of SANDBOX_FIELDS) {\n if (cfg[field] !== undefined) needs.push(`${field} needs a sandbox`)\n }\n if (cfg.maxRounds !== undefined && cfg.maxRounds > 1)\n needs.push('maxRounds > 1 needs a multi-round session')\n if (cfg.ask === true) needs.push('ask needs a session to answer into')\n const refusal = profile ? loopProfileRefusal(profile, options) : undefined\n if (refusal) needs.push(refusal)\n // Inference and the loop run the profile's own model and refuse another; only the\n // sandbox decides between a requested model and the profile's.\n const profileModel = profileDefaultModel(profile)\n if (cfg.model !== undefined && profileModel !== undefined && cfg.model !== profileModel) {\n needs.push(\n `the action model ${cfg.model} differs from the profile model ${String(profileModel)}`,\n )\n }\n return needs\n}\n\nfunction profileDefaultModel(profile: object | undefined): unknown {\n return (profile as { model?: { default?: unknown } } | undefined)?.model?.default\n}\n\n/** Options for {@link sandboxOnlyRequirements}. */\nexport interface SandboxOnlyRequirementsOptions extends LoopProfileRefusalOptions {\n requirements?: PlacementRequirements\n profile?: AgentProfile | object\n interactive: boolean\n}\n\n/**\n * Every declared requirement only a sandbox can meet. A caller resuming work in the\n * loop it began in checks this is empty before it continues there.\n */\nexport function sandboxOnlyRequirements(options: SandboxOnlyRequirementsOptions): string[] {\n return sandboxRequirements({ ...options, requirements: options.requirements ?? {} })\n}\n\n/** Tools the work grants, each of which the loop can hold. */\nfunction toolRequirements(cfg: PlacementRequirements): string[] {\n const tools: string[] = []\n if ((cfg.connections ?? []).length > 0) tools.push('connections need Hub tools')\n if (cfg.objects !== undefined) tools.push('objects need object tools')\n if (cfg.http !== undefined) tools.push('http needs a fetch tool')\n if ((cfg.tools ?? []).length > 0) tools.push(`tools ${cfg.tools?.join(', ')} need the loop`)\n return tools\n}\n\n/** Grants only the loop holds: a sandbox cannot honour them. */\nfunction loopOnlyGrants(cfg: PlacementRequirements): string[] {\n return [\n ...(cfg.objects !== undefined ? ['objects'] : []),\n ...(cfg.http !== undefined ? ['http'] : []),\n ]\n}\n\n/** True when the profile declares skills that only an inliner on the loop can carry. */\nfunction declaresProfileSkills(profile: object | undefined): boolean {\n const fields = (profile ?? {}) as {\n resources?: { skills?: unknown }\n tools?: Record<string, unknown>\n permissions?: Record<string, unknown>\n }\n const skills = fields.resources?.skills\n return (\n (Array.isArray(skills) && skills.length > 0) ||\n fields.tools?.skill === true ||\n (fields.permissions?.skill !== undefined && fields.permissions.skill !== 'deny')\n )\n}\n\n/**\n * Choose where work runs: the cheapest tier every declared requirement allows.\n * Throws {@link PlacementRefusedError} when no tier can meet every requirement.\n */\nexport function placeExecution(input: PlaceExecutionInput): PlacementRecord {\n const cfg = input.requirements ?? {}\n const { context, profile } = input\n const opts: LoopProfileRefusalOptions = {\n ...(input.vacuousAxes ? { vacuousAxes: input.vacuousAxes } : {}),\n ...(input.inlineSkills ? { inlineSkills: true } : {}),\n }\n const sandboxNeeds = sandboxRequirements({\n requirements: cfg,\n profile,\n interactive: context.interactive,\n ...opts,\n })\n const tools = toolRequirements(cfg)\n if (input.inlineSkills && declaresProfileSkills(profile))\n tools.push('skills are inlined by the loop')\n const loopOnly = loopOnlyGrants(cfg)\n const coveredAxes = profile ? declaredAxes(profile) : []\n const record = (\n chosen: ExecutionTier,\n reason: string,\n rejected: PlacementRecord['rejected'],\n ): PlacementRecord => ({ chosen, policy: PLACEMENT_POLICY, reason, coveredAxes, rejected })\n\n if (sandboxNeeds.length > 0) {\n const reason = sandboxNeeds.join('; ')\n if (loopOnly.length > 0) {\n throw new PlacementRefusedError(\n `${loopOnly.join(' and ')} run only in the worker, but ${reason}`,\n )\n }\n return record('sandbox', reason, [\n { placement: 'inference', reason },\n { placement: 'loop', reason },\n ])\n }\n\n // Past the sandbox check the profile is one the loop (and a single Router call) can\n // honour, so only granted tools separate the two.\n if (tools.length > 0) {\n const reason = `${tools.join('; ')}, which the worker holds without a sandbox`\n if (!context.loopAvailable) {\n const unavailable =\n context.loopUnavailableReason ?? 'worker placement is not wired on this deployment'\n if (loopOnly.length > 0) {\n throw new PlacementRefusedError(\n `${loopOnly.join(' and ')} need the worker, but ${unavailable}`,\n )\n }\n // Inlined skills are the one loop need a sandbox also meets, natively.\n return record('sandbox', unavailable, [\n { placement: 'inference', reason: tools.join('; ') },\n { placement: 'loop', reason: unavailable },\n ])\n }\n return record('loop', reason, [\n { placement: 'inference', reason: tools.join('; ') },\n { placement: 'sandbox', reason: 'costs more and no declared requirement needs it' },\n ])\n }\n\n if (context.inferenceAvailable) {\n return record('inference', 'no declared requirement needs tools, files or a session', [\n { placement: 'loop', reason: 'no declared tool needs a loop' },\n { placement: 'sandbox', reason: 'costs more and no declared requirement needs it' },\n ])\n }\n const unwired = 'inference is not wired on this deployment'\n if (context.loopAvailable) {\n return record('loop', `${unwired}; one tool-free worker turn`, [\n { placement: 'inference', reason: unwired },\n { placement: 'sandbox', reason: 'costs more and no declared requirement needs it' },\n ])\n }\n return record('sandbox', unwired, [\n { placement: 'inference', reason: unwired },\n { placement: 'loop', reason: 'worker placement is not wired on this deployment' },\n ])\n}\n\n/** The materialization axes a profile declares, in its own field order. Read from the\n * same field map as {@link loopProfileRefusal}, so the module has no runtime import. */\nfunction declaredAxes(profile: object): AgentProfileMaterializationAxis[] {\n const axes: AgentProfileMaterializationAxis[] = []\n for (const [field, value] of Object.entries(profile)) {\n if (value === undefined) continue\n if (TOP_LEVEL_AXES.has(field)) axes.push(field as AgentProfileMaterializationAxis)\n const children = COMPOUND_FIELDS[field]\n if (!children || typeof value !== 'object' || value === null) continue\n for (const [child, inner] of Object.entries(value)) {\n const axis = children[child]\n if (axis !== undefined && inner !== undefined) axes.push(axis)\n }\n }\n return axes\n}\n\n/** The record for a tier the author named: nothing was chosen or refused. */\nexport function authoredPlacement(chosen: ExecutionTier): PlacementRecord {\n return {\n chosen,\n policy: PLACEMENT_POLICY,\n reason: `authored placement: ${chosen}`,\n coveredAxes: [],\n rejected: [],\n }\n}\n\n/**\n * The record for work that resumes under the tier it began with. A retry reuses the\n * recorded tier and never re-decides, so one turn never runs on two tiers; a caller\n * resuming on `loop` first checks {@link sandboxOnlyRequirements} is empty.\n */\nexport function resumedPlacement(chosen: ExecutionTier): PlacementRecord {\n return {\n chosen,\n policy: PLACEMENT_POLICY,\n reason: `resumed under the placement it began with: ${chosen}`,\n coveredAxes: [],\n rejected: [],\n }\n}\nexport type {\n AgentProfileMaterializationAxis,\n KnownAgentProfileMaterializationAxis,\n} from '../agent/profile-materialization'\n"],"mappings":"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"}
@@ -1,4 +1,3 @@
1
- import { a as HealthCheck, c as PlatformCatalogProvider, d as PlatformHubClientOptions, f as PlatformHubError, h as StartAuthResult, i as ExecInput, l as PlatformConnection, m as StartAuthInput, n as ConnectionHealth, o as MintTokenInput, p as PlatformHubStatus, r as ConnectionHealthResult, s as MintTokenResult, t as CatalogResult, u as PlatformHubClient } from "./integrations-6xttbPi9.js";
2
1
  //#region src/platform/auth.d.ts
3
2
  /**
4
3
  * Server-side client for the Tangle platform's cross-site SSO bridge.
@@ -73,6 +72,194 @@ declare class PlatformAuthClient {
73
72
  exchange(code: string): Promise<ExchangeCodeResult>;
74
73
  }
75
74
  //#endregion
75
+ //#region src/platform/integrations.d.ts
76
+ /**
77
+ * Server-side client for the Tangle platform's integration hub
78
+ * (`/v1/hub/*`). Consumer apps use this instead of rolling their own
79
+ * OAuth + connection tables.
80
+ *
81
+ * Auth: the caller supplies a bearer (either the user's API key from
82
+ * cross-site exchange, or a platform service token) on construction.
83
+ *
84
+ * Endpoint contract (authoritative): the platform API's hub contract
85
+ * and hub route (both in agent-container's platform API). The platform wraps every response in
86
+ * `{ success, data }`; non-2xx or `success:false` surfaces as `PlatformHubError`
87
+ * carrying the real upstream status.
88
+ */
89
+ interface PlatformHubClientOptions {
90
+ /** Platform base URL, e.g. `https://id.tangle.tools`. */
91
+ baseUrl: string;
92
+ /** Bearer credential — user API key or service token. */
93
+ bearer: string;
94
+ /** Override fetch (tests + edge runtimes). */
95
+ fetchImpl?: typeof fetch;
96
+ }
97
+ /** A live integration connection, as returned by `/v1/hub/connections`. */
98
+ interface PlatformConnection {
99
+ id: string;
100
+ providerId: string;
101
+ displayName: string;
102
+ accountDisplay: string | null;
103
+ scopes: string[];
104
+ status: 'active' | 'revoked' | 'unhealthy' | 'reconnect_required' | (string & {});
105
+ health: 'unknown' | 'healthy' | 'unhealthy' | 'rate_limited' | (string & {});
106
+ createdAt: string;
107
+ updatedAt: string;
108
+ lastUsedAt: string | null;
109
+ }
110
+ /** A connectable provider in the catalog (`/v1/hub/providers`). */
111
+ interface PlatformCatalogProvider {
112
+ providerId: string;
113
+ title?: string;
114
+ authKind?: string;
115
+ category?: string;
116
+ scopes?: string[];
117
+ capabilityCount?: number;
118
+ native?: boolean;
119
+ /** Whether the OAuth app's credentials are wired — the UI offers Connect
120
+ * only when true. */
121
+ configured?: boolean;
122
+ [k: string]: unknown;
123
+ }
124
+ interface CatalogResult {
125
+ providers: PlatformCatalogProvider[];
126
+ /** Count of substrate-bundled connectors behind the catalog. */
127
+ substrateBundled?: number;
128
+ [k: string]: unknown;
129
+ }
130
+ interface StartAuthInput {
131
+ /** The provider to connect (goes in the URL path). */
132
+ providerId: string;
133
+ /** Accepted for interface compatibility; the platform's start endpoint is
134
+ * provider-level and does not consume a connector id. */
135
+ connectorId?: string;
136
+ /** Where the platform redirects the user back to after OAuth. */
137
+ returnUrl: string;
138
+ /** Accepted for interface compatibility; not consumed by the start endpoint. */
139
+ requestedScopes?: string[];
140
+ /** CLI flow flag — affects the platform's post-auth redirect handling. */
141
+ cli?: boolean;
142
+ }
143
+ interface StartAuthResult {
144
+ /** The URL to send the user to. Normalized across the platform's two start
145
+ * branches: github returns `authorizationUrl`, substrate returns
146
+ * `redirectUrl`. */
147
+ authorizationUrl: string;
148
+ state: string;
149
+ expiresAt?: string;
150
+ scopes?: string[];
151
+ }
152
+ interface ConnectionHealth {
153
+ status: 'unknown' | 'healthy' | 'unhealthy' | 'rate_limited' | (string & {});
154
+ checkedAt: string;
155
+ error?: {
156
+ code: string;
157
+ message: string;
158
+ };
159
+ }
160
+ interface ConnectionHealthResult {
161
+ connection: PlatformConnection;
162
+ health: ConnectionHealth;
163
+ }
164
+ /** Last-known health for a connection, derived from the connection row. */
165
+ interface HealthCheck {
166
+ connectionId: string;
167
+ providerId: string;
168
+ /** Mirrors `PlatformConnection.health`. */
169
+ status: ConnectionHealth['status'];
170
+ checkedAt?: string;
171
+ }
172
+ interface MintTokenInput {
173
+ /** The hub action the token authorizes (e.g. `slack.chat.postMessage`). */
174
+ actionPath: string;
175
+ /** Bind to a specific connection, or … */
176
+ connectionId?: string;
177
+ /** … resolve the connection by provider for the calling user. */
178
+ provider?: string;
179
+ }
180
+ interface MintTokenResult {
181
+ tokenId: string;
182
+ token: string;
183
+ expiresAt: string;
184
+ }
185
+ interface ExecInput {
186
+ /** The hub action path to execute. */
187
+ path: string;
188
+ input?: unknown;
189
+ connectionId?: string;
190
+ }
191
+ interface PlatformHubStatus {
192
+ contract?: unknown;
193
+ principal: {
194
+ kind: string;
195
+ userId: string;
196
+ [k: string]: unknown;
197
+ };
198
+ connections: {
199
+ connectedProviderCount: number;
200
+ unhealthyProviderCount: number;
201
+ };
202
+ }
203
+ /** Thrown when a `PlatformHubClient` request returns a non-success status. */
204
+ declare class PlatformHubError extends Error {
205
+ readonly status: number;
206
+ readonly code: string | undefined;
207
+ readonly body: unknown;
208
+ constructor(message: string, status: number, code: string | undefined, body: unknown);
209
+ }
210
+ /** HTTP client for the Tangle Platform Hub API: provider catalog, connection flow, and status. */
211
+ declare class PlatformHubClient {
212
+ private readonly baseUrl;
213
+ private readonly bearer;
214
+ private readonly fetchImpl;
215
+ constructor(options: PlatformHubClientOptions);
216
+ /** GET /v1/hub/providers — the connectable provider catalog. */
217
+ catalog(): Promise<CatalogResult>;
218
+ /** GET /v1/hub/connections — the calling user's live connections. */
219
+ listConnections(): Promise<PlatformConnection[]>;
220
+ /** DELETE /v1/hub/connections/:connectionId — revoke + disable a connection. */
221
+ revokeConnection(connectionId: string): Promise<{
222
+ connection: PlatformConnection;
223
+ }>;
224
+ /**
225
+ * POST /v1/hub/connections/:provider/start — begin OAuth/grant. The provider
226
+ * is taken from the URL; the body carries `returnUrl` (+ `cli`). The platform's
227
+ * two start branches name the URL field differently (github → `authorizationUrl`,
228
+ * substrate → `redirectUrl`); this normalizes to `authorizationUrl`.
229
+ */
230
+ startAuth(input: StartAuthInput): Promise<StartAuthResult>;
231
+ /**
232
+ * Last-known health for every connection. The platform has no global
233
+ * healthcheck listing — health rides on each connection row — so this derives
234
+ * the list from `listConnections()` (one request, no extra round-trips).
235
+ */
236
+ listHealthchecks(): Promise<HealthCheck[]>;
237
+ /**
238
+ * POST /v1/hub/connections/:connectionId/health — trigger a fresh health
239
+ * probe for one connection and return its updated state.
240
+ */
241
+ checkConnectionHealth(connectionId: string): Promise<ConnectionHealthResult>;
242
+ /**
243
+ * Trigger a fresh health probe across all of the user's connections. The
244
+ * platform exposes health per-connection only, so this fans out over
245
+ * `listConnections()`. `scheduled` is the number of probes dispatched.
246
+ */
247
+ runHealthchecks(): Promise<{
248
+ scheduled: number;
249
+ }>;
250
+ /** GET /v1/hub/status — principal + aggregate connection counts. */
251
+ status(): Promise<PlatformHubStatus>;
252
+ /**
253
+ * POST /v1/hub/tokens — mint a short-lived, action-scoped capability token a
254
+ * sandbox can use to invoke one hub action on the user's behalf without
255
+ * seeing the underlying provider credential.
256
+ */
257
+ mintToken(input: MintTokenInput): Promise<MintTokenResult>;
258
+ /** POST /v1/hub/exec — execute a hub action and return its result. */
259
+ exec(input: ExecInput): Promise<unknown>;
260
+ private request;
261
+ }
262
+ //#endregion
76
263
  //#region src/platform/oidc.d.ts
77
264
  /**
78
265
  * OpenID Connect client for the Tangle authorization server. Identity tokens
package/dist/platform.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { t as isRecord } from "./record-value-B6bwJxLa.js";
2
- import { n as PlatformHubError, t as PlatformHubClient } from "./integrations-dFi-ALk7.js";
3
2
  //#region src/platform/auth.ts
4
3
  /**
5
4
  * Server-side client for the Tangle platform's cross-site SSO bridge.
@@ -103,6 +102,135 @@ var PlatformAuthClient = class {
103
102
  }
104
103
  };
105
104
  //#endregion
105
+ //#region src/platform/integrations.ts
106
+ /** Thrown when a `PlatformHubClient` request returns a non-success status. */
107
+ var PlatformHubError = class extends Error {
108
+ status;
109
+ code;
110
+ body;
111
+ constructor(message, status, code, body) {
112
+ super(message);
113
+ this.status = status;
114
+ this.code = code;
115
+ this.body = body;
116
+ this.name = "PlatformHubError";
117
+ }
118
+ };
119
+ /** HTTP client for the Tangle Platform Hub API: provider catalog, connection flow, and status. */
120
+ var PlatformHubClient = class {
121
+ baseUrl;
122
+ bearer;
123
+ fetchImpl;
124
+ constructor(options) {
125
+ if (!options.baseUrl) throw new Error("PlatformHubClient: baseUrl is required");
126
+ if (!options.bearer) throw new Error("PlatformHubClient: bearer is required");
127
+ this.baseUrl = options.baseUrl.replace(/\/+$/, "");
128
+ this.bearer = options.bearer;
129
+ this.fetchImpl = options.fetchImpl ?? ((url, init) => fetch(url, init));
130
+ }
131
+ /** GET /v1/hub/providers — the connectable provider catalog. */
132
+ catalog() {
133
+ return this.request("GET", "/v1/hub/providers");
134
+ }
135
+ /** GET /v1/hub/connections — the calling user's live connections. */
136
+ async listConnections() {
137
+ return (await this.request("GET", "/v1/hub/connections")).connections;
138
+ }
139
+ /** DELETE /v1/hub/connections/:connectionId — revoke + disable a connection. */
140
+ revokeConnection(connectionId) {
141
+ return this.request("DELETE", `/v1/hub/connections/${encodeURIComponent(connectionId)}`);
142
+ }
143
+ /**
144
+ * POST /v1/hub/connections/:provider/start — begin OAuth/grant. The provider
145
+ * is taken from the URL; the body carries `returnUrl` (+ `cli`). The platform's
146
+ * two start branches name the URL field differently (github → `authorizationUrl`,
147
+ * substrate → `redirectUrl`); this normalizes to `authorizationUrl`.
148
+ */
149
+ async startAuth(input) {
150
+ const body = { returnUrl: input.returnUrl };
151
+ if (input.cli !== void 0) body.cli = input.cli;
152
+ const data = await this.request("POST", `/v1/hub/connections/${encodeURIComponent(input.providerId)}/start`, body);
153
+ const authorizationUrl = data.authorizationUrl ?? data.redirectUrl;
154
+ if (!authorizationUrl) throw new PlatformHubError("Platform hub start response missing an authorization URL", 502, "HUB_INVALID_START_RESPONSE", data);
155
+ return {
156
+ authorizationUrl,
157
+ state: data.state,
158
+ expiresAt: data.expiresAt,
159
+ scopes: data.scopes
160
+ };
161
+ }
162
+ /**
163
+ * Last-known health for every connection. The platform has no global
164
+ * healthcheck listing — health rides on each connection row — so this derives
165
+ * the list from `listConnections()` (one request, no extra round-trips).
166
+ */
167
+ async listHealthchecks() {
168
+ return (await this.listConnections()).map((c) => ({
169
+ connectionId: c.id,
170
+ providerId: c.providerId,
171
+ status: c.health,
172
+ checkedAt: c.updatedAt
173
+ }));
174
+ }
175
+ /**
176
+ * POST /v1/hub/connections/:connectionId/health — trigger a fresh health
177
+ * probe for one connection and return its updated state.
178
+ */
179
+ checkConnectionHealth(connectionId) {
180
+ return this.request("POST", `/v1/hub/connections/${encodeURIComponent(connectionId)}/health`);
181
+ }
182
+ /**
183
+ * Trigger a fresh health probe across all of the user's connections. The
184
+ * platform exposes health per-connection only, so this fans out over
185
+ * `listConnections()`. `scheduled` is the number of probes dispatched.
186
+ */
187
+ async runHealthchecks() {
188
+ const connections = await this.listConnections();
189
+ await Promise.allSettled(connections.map((c) => this.checkConnectionHealth(c.id)));
190
+ return { scheduled: connections.length };
191
+ }
192
+ /** GET /v1/hub/status — principal + aggregate connection counts. */
193
+ status() {
194
+ return this.request("GET", "/v1/hub/status");
195
+ }
196
+ /**
197
+ * POST /v1/hub/tokens — mint a short-lived, action-scoped capability token a
198
+ * sandbox can use to invoke one hub action on the user's behalf without
199
+ * seeing the underlying provider credential.
200
+ */
201
+ mintToken(input) {
202
+ return this.request("POST", "/v1/hub/tokens", input);
203
+ }
204
+ /** POST /v1/hub/exec — execute a hub action and return its result. */
205
+ async exec(input) {
206
+ return (await this.request("POST", "/v1/hub/exec", input)).result;
207
+ }
208
+ async request(method, path, body) {
209
+ const headers = {
210
+ authorization: `Bearer ${this.bearer}`,
211
+ accept: "application/json"
212
+ };
213
+ if (body !== void 0) headers["content-type"] = "application/json";
214
+ const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
215
+ method,
216
+ headers,
217
+ body: body !== void 0 ? JSON.stringify(body) : void 0
218
+ });
219
+ const text = await res.text();
220
+ let parsed = null;
221
+ if (text) try {
222
+ parsed = JSON.parse(text);
223
+ } catch {}
224
+ if (!res.ok || parsed && parsed.success === false) {
225
+ const code = parsed?.error && typeof parsed.error === "object" ? parsed.error.code : void 0;
226
+ throw new PlatformHubError(parsed?.error && typeof parsed.error === "object" && parsed.error.message || (typeof parsed?.error === "string" ? parsed.error : `Platform hub error (${res.status})`), res.status, code, parsed ?? text);
227
+ }
228
+ if (!parsed) throw new PlatformHubError(`Platform hub returned non-JSON success (${res.status})`, res.status, void 0, text);
229
+ if (parsed.data === void 0) throw new PlatformHubError("Platform hub envelope missing `data`", res.status, void 0, parsed);
230
+ return parsed.data;
231
+ }
232
+ };
233
+ //#endregion
106
234
  //#region src/platform/oidc.ts
107
235
  /**
108
236
  * OpenID Connect client for the Tangle authorization server. Identity tokens