induscode 0.1.0__py3-none-any.whl

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 (167) hide show
  1. induscode/__init__.py +56 -0
  2. induscode/addons/__init__.py +176 -0
  3. induscode/addons/contract.py +923 -0
  4. induscode/addons/dispatch/__init__.py +43 -0
  5. induscode/addons/dispatch/event_dispatcher.py +348 -0
  6. induscode/addons/dispatch/tool_interceptor.py +349 -0
  7. induscode/addons/host.py +469 -0
  8. induscode/addons/loader.py +314 -0
  9. induscode/addons/manifest.py +232 -0
  10. induscode/addons/surface.py +199 -0
  11. induscode/boot/__init__.py +108 -0
  12. induscode/boot/auth_vault.py +323 -0
  13. induscode/boot/boot.py +210 -0
  14. induscode/boot/contract.py +223 -0
  15. induscode/boot/invocation.py +117 -0
  16. induscode/boot/runners/__init__.py +42 -0
  17. induscode/boot/runners/link_runner.py +82 -0
  18. induscode/boot/runners/oneshot_runner.py +85 -0
  19. induscode/boot/runners/registry.py +46 -0
  20. induscode/boot/runners/repl_runner.py +340 -0
  21. induscode/boot/runners/session.py +549 -0
  22. induscode/boot/stages.py +198 -0
  23. induscode/boot/upgrade/__init__.py +36 -0
  24. induscode/boot/upgrade/apply.py +125 -0
  25. induscode/boot/upgrade/upgrades.py +136 -0
  26. induscode/briefing/__init__.py +115 -0
  27. induscode/briefing/compose.py +414 -0
  28. induscode/briefing/contract.py +528 -0
  29. induscode/briefing/macros.py +721 -0
  30. induscode/briefing/skills.py +417 -0
  31. induscode/capability_deck/__init__.py +233 -0
  32. induscode/capability_deck/bridge_ledger/__init__.py +66 -0
  33. induscode/capability_deck/bridge_ledger/key.py +181 -0
  34. induscode/capability_deck/bridge_ledger/ledger.py +276 -0
  35. induscode/capability_deck/bridge_ledger/network.py +336 -0
  36. induscode/capability_deck/builtin_bridge.py +358 -0
  37. induscode/capability_deck/cards/__init__.py +116 -0
  38. induscode/capability_deck/cards/bg_process.py +482 -0
  39. induscode/capability_deck/cards/memory.py +226 -0
  40. induscode/capability_deck/cards/saas.py +280 -0
  41. induscode/capability_deck/cards/task.py +256 -0
  42. induscode/capability_deck/cards/todo.py +312 -0
  43. induscode/capability_deck/contract.py +450 -0
  44. induscode/capability_deck/manifest.py +126 -0
  45. induscode/capability_deck/provision.py +217 -0
  46. induscode/channels/__init__.py +146 -0
  47. induscode/channels/contract.py +585 -0
  48. induscode/channels/framer.py +132 -0
  49. induscode/channels/link/__init__.py +50 -0
  50. induscode/channels/link/dialog.py +246 -0
  51. induscode/channels/link/driver.py +308 -0
  52. induscode/channels/link/server.py +217 -0
  53. induscode/channels/oneshot.py +178 -0
  54. induscode/channels/ops.py +140 -0
  55. induscode/channels/session_ops.py +172 -0
  56. induscode/conductor/__init__.py +240 -0
  57. induscode/conductor/catalog.py +309 -0
  58. induscode/conductor/conductor.py +1084 -0
  59. induscode/conductor/contract.py +1035 -0
  60. induscode/conductor/matcher.py +291 -0
  61. induscode/conductor/serialize.py +575 -0
  62. induscode/conductor/signal_hub.py +382 -0
  63. induscode/conductor/skill_parse.py +294 -0
  64. induscode/conductor/transcript_store.py +449 -0
  65. induscode/console/__init__.py +236 -0
  66. induscode/console/app.py +1677 -0
  67. induscode/console/components/__init__.py +62 -0
  68. induscode/console/components/banner.py +499 -0
  69. induscode/console/components/banner_sweep.py +188 -0
  70. induscode/console/components/emblem.py +181 -0
  71. induscode/console/components/status_bar.py +102 -0
  72. induscode/console/contract.py +836 -0
  73. induscode/console/input/__init__.py +107 -0
  74. induscode/console/input/chord.py +197 -0
  75. induscode/console/input/dir_reader.py +113 -0
  76. induscode/console/input/intents.py +258 -0
  77. induscode/console/input/providers.py +469 -0
  78. induscode/console/mount.py +137 -0
  79. induscode/console/overlays/__init__.py +94 -0
  80. induscode/console/overlays/auth.py +503 -0
  81. induscode/console/overlays/pickers.py +526 -0
  82. induscode/console/overlays/router.py +129 -0
  83. induscode/console/overlays/sessions.py +232 -0
  84. induscode/console/reducer.py +145 -0
  85. induscode/console/resume_picker.py +156 -0
  86. induscode/console/slash_commands/__init__.py +78 -0
  87. induscode/console/slash_commands/builtins.py +254 -0
  88. induscode/console/slash_commands/dynamic.py +217 -0
  89. induscode/console/slash_commands/integrations.py +949 -0
  90. induscode/console/slash_commands/transcript.py +404 -0
  91. induscode/console/slash_commands/workbench.py +430 -0
  92. induscode/console/startup.py +434 -0
  93. induscode/console/theme/__init__.py +44 -0
  94. induscode/console/theme/adapter.py +168 -0
  95. induscode/console/theme/palette.py +128 -0
  96. induscode/console/theme/resolve.py +123 -0
  97. induscode/console/theme/tokens.py +185 -0
  98. induscode/console_slash/__init__.py +111 -0
  99. induscode/console_slash/contract.py +185 -0
  100. induscode/console_slash/registry.py +140 -0
  101. induscode/console_slash/resolve.py +194 -0
  102. induscode/console_slash/shared.py +172 -0
  103. induscode/entry.py +108 -0
  104. induscode/insight/__init__.py +153 -0
  105. induscode/insight/collector.py +73 -0
  106. induscode/insight/replay.py +305 -0
  107. induscode/insight/wrapper.py +1115 -0
  108. induscode/kit/__init__.py +82 -0
  109. induscode/kit/clipboard_image.py +215 -0
  110. induscode/kit/external_editor.py +120 -0
  111. induscode/kit/image.py +188 -0
  112. induscode/kit/shell.py +89 -0
  113. induscode/kit/tool_fetch.py +288 -0
  114. induscode/launch/__init__.py +224 -0
  115. induscode/launch/catalog.py +310 -0
  116. induscode/launch/contract.py +569 -0
  117. induscode/launch/credentials.py +852 -0
  118. induscode/launch/invocation/__init__.py +39 -0
  119. induscode/launch/invocation/attachments.py +281 -0
  120. induscode/launch/invocation/flags.py +210 -0
  121. induscode/launch/invocation/read.py +369 -0
  122. induscode/launch/invocation/usage.py +110 -0
  123. induscode/launch/oauth.py +808 -0
  124. induscode/launch/packages.py +299 -0
  125. induscode/launch/pickers.py +291 -0
  126. induscode/py.typed +0 -0
  127. induscode/runtime_bridge/__init__.py +166 -0
  128. induscode/runtime_bridge/bridges/__init__.py +66 -0
  129. induscode/runtime_bridge/bridges/_drive.py +268 -0
  130. induscode/runtime_bridge/bridges/builtins.py +177 -0
  131. induscode/runtime_bridge/bridges/claude_cli.py +198 -0
  132. induscode/runtime_bridge/bridges/codex_cli.py +203 -0
  133. induscode/runtime_bridge/bridges/indusagi_cli.py +217 -0
  134. induscode/runtime_bridge/broker.py +397 -0
  135. induscode/runtime_bridge/contract.py +734 -0
  136. induscode/runtime_bridge/sink.py +351 -0
  137. induscode/sessions/__init__.py +25 -0
  138. induscode/sessions/contract.py +119 -0
  139. induscode/sessions/library.py +350 -0
  140. induscode/settings/__init__.py +47 -0
  141. induscode/settings/contract.py +313 -0
  142. induscode/settings/manager.py +268 -0
  143. induscode/transcript_export/__init__.py +109 -0
  144. induscode/transcript_export/contract.py +522 -0
  145. induscode/transcript_export/publish.py +455 -0
  146. induscode/transcript_export/sgr.py +566 -0
  147. induscode/transcript_export/template.py +319 -0
  148. induscode/transcript_export/theme_bridge.py +325 -0
  149. induscode/window_budget/__init__.py +76 -0
  150. induscode/window_budget/budget/__init__.py +26 -0
  151. induscode/window_budget/budget/estimate.py +273 -0
  152. induscode/window_budget/budget/gate.py +60 -0
  153. induscode/window_budget/budget/slice.py +145 -0
  154. induscode/window_budget/condenser.py +170 -0
  155. induscode/window_budget/contract.py +329 -0
  156. induscode/window_budget/summarize/__init__.py +33 -0
  157. induscode/window_budget/summarize/condense.py +212 -0
  158. induscode/window_budget/summarize/prompt.py +241 -0
  159. induscode/workspace/__init__.py +30 -0
  160. induscode/workspace/brand.py +96 -0
  161. induscode/workspace/locator.py +269 -0
  162. induscode-0.1.0.dist-info/METADATA +97 -0
  163. induscode-0.1.0.dist-info/RECORD +167 -0
  164. induscode-0.1.0.dist-info/WHEEL +4 -0
  165. induscode-0.1.0.dist-info/entry_points.txt +3 -0
  166. induscode-0.1.0.dist-info/licenses/CREDITS.md +22 -0
  167. induscode-0.1.0.dist-info/licenses/NOTICE +7 -0
@@ -0,0 +1,336 @@
1
+ """The bridge network — mounting external MCP servers into the ledger.
2
+
3
+ This is the side-effecting half of bridge enrollment. The framework's
4
+ :func:`indusagi.interop.mount_protocol_bridge` does the protocol work: it
5
+ connects every configured server, lists each ready endpoint's tools, and hands
6
+ back a :class:`~indusagi.interop.MountedProtocolBridge` whose ``box``
7
+ (:class:`~indusagi.runtime.ToolBox`) advertises every grafted remote tool
8
+ under its qualified ``"<server>__<tool>"`` name through ``descriptors()`` and
9
+ whose shared ``runner`` routes a call back across the owning endpoint.
10
+
11
+ What this module adds on top:
12
+
13
+ - **Adaptation.** A ``ToolBox`` speaks the runtime's descriptor/runner
14
+ contract; the conductor and catalog speak :data:`Capability` (the framework
15
+ ``AgentTool``). :func:`bridge_box_to_capabilities` bridges the two, wrapping
16
+ each descriptor + the shared runner into an ``AgentTool`` whose ``execute``
17
+ invokes the runner and projects its opaque outcome onto an
18
+ :class:`AgentToolResult` (a single text block).
19
+ - **Enrollment.** :func:`attach_bridge_capabilities` folds those adapted
20
+ capabilities into a :class:`BridgeLedger` as ``enroll`` events, returning
21
+ the new ledger, the live fleet (for teardown), and the typed status — never
22
+ mutating a shared list. A wholesale mount failure becomes a ``bridge``
23
+ :class:`DeckFault` ON the result, never raised, so a bad MCP configuration
24
+ degrades the deck instead of sinking session bootstrap.
25
+ - **Cataloging.** :func:`bridge_capability_card` re-presents a grafted
26
+ capability as a :class:`CapabilityCard`, so dynamic MCP tools surface in
27
+ help/introspection beside the static catalog rows.
28
+
29
+ Failure isolation comes for free from the fleet: a server that faulted on
30
+ connect contributes no descriptors and is simply absent from the enrollment.
31
+
32
+ Port note — cancellation tokens: the runtime ``ToolRunner.run`` requires a
33
+ ``CancelToken`` (the TS ``AbortSignal``); the framework exports no public
34
+ constructor for one, so — like the workspace locator's use of
35
+ ``indusagi._internal.env`` — the adapter mints a placeholder from
36
+ ``indusagi._internal.cancel`` when the caller passes no signal (the analogue
37
+ of the TS ``new AbortController().signal``).
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import json
43
+ from collections.abc import Sequence
44
+ from dataclasses import dataclass
45
+
46
+ from indusagi._internal.cancel import CancelToken
47
+ from indusagi.ai import TextContent
48
+ from indusagi.interop import (
49
+ BridgeConfig,
50
+ FleetStatus,
51
+ ServerConfig,
52
+ ServerFleet,
53
+ is_protocol_fault,
54
+ )
55
+ from indusagi.interop import mount_protocol_bridge as _mount_protocol_bridge
56
+ from indusagi.runtime import ToolBox, ToolCall
57
+
58
+ from ..contract import (
59
+ AgentToolResult,
60
+ AnyCapability,
61
+ CapabilityCard,
62
+ DeckFault,
63
+ Schema,
64
+ capability_id,
65
+ deck_fault,
66
+ )
67
+ from .ledger import BridgeLedger, EnrollRequest, enroll_bridge_card, withdraw_server
68
+
69
+ __all__ = [
70
+ "AttachResult",
71
+ "attach_bridge_capabilities",
72
+ "bridge_box_to_capabilities",
73
+ "bridge_capability_card",
74
+ "bridge_config",
75
+ "detach_bridge",
76
+ ]
77
+
78
+
79
+ def _server_of_qualified(name: str) -> str:
80
+ """One descriptor's server id, recovered from its qualified name."""
81
+ sep = name.find("__")
82
+ return name[:sep] if sep > 0 else ""
83
+
84
+
85
+ def _safe_stringify(value: object) -> str:
86
+ """JSON-encode a value, degrading to ``str(value)`` if it is not
87
+ serializable (the TS ``JSON.stringify`` try/catch)."""
88
+ try:
89
+ return json.dumps(value, indent=2, ensure_ascii=False)
90
+ except Exception:
91
+ return str(value)
92
+
93
+
94
+ def _project_outcome(output: object, is_error: bool) -> AgentToolResult:
95
+ """Project a remote tool's opaque outcome onto an :class:`AgentToolResult`.
96
+
97
+ The bridged ``ToolBox`` runner returns a runtime ``ToolOutcome`` whose
98
+ ``output`` is opaque (an MCP server may return text, structured JSON, or a
99
+ mix of content blocks). The capability contract requires content blocks,
100
+ so we render the output into a single text block, preserving plain strings
101
+ verbatim and JSON-encoding anything structured. The ``is_error`` flag
102
+ rides straight through.
103
+
104
+ :param output: the runner's opaque tool output
105
+ :param is_error: whether the remote tool reported its own failure
106
+ """
107
+ if isinstance(output, str):
108
+ text = output
109
+ elif output is None:
110
+ text = ""
111
+ else:
112
+ text = _safe_stringify(output)
113
+ return AgentToolResult(
114
+ content=(TextContent(text=text),),
115
+ details=output,
116
+ isError=is_error,
117
+ )
118
+
119
+
120
+ # ---------------------------------------------------------------------------
121
+ # Adaptation: ToolBox descriptor/runner → AgentTool capability
122
+ # ---------------------------------------------------------------------------
123
+
124
+
125
+ class _BridgedCapability:
126
+ """One grafted remote tool, adapted to the ``AgentTool`` shape.
127
+
128
+ The descriptor's qualified name, description, and parameter schema are
129
+ carried verbatim; ``execute`` defers to the box's shared runner — passing
130
+ the cancel token through and projecting the outcome with
131
+ :func:`_project_outcome`. The runner already knows how to route a call by
132
+ name back to the owning endpoint, so the wrapper stays a thin shim.
133
+ """
134
+
135
+ __slots__ = ("name", "label", "description", "parameters", "_runner")
136
+
137
+ def __init__(
138
+ self, name: str, description: str, parameters: Schema, runner: object
139
+ ) -> None:
140
+ self.name = name
141
+ # The model-facing label doubles as the qualified name; it is unique
142
+ # across servers so it serves as a stable display handle too.
143
+ self.label = name
144
+ self.description = description
145
+ # The bridge already normalized the remote schema to the provider-safe
146
+ # subset; we thread it through as the capability's parameters mapping.
147
+ self.parameters = parameters
148
+ self._runner = runner
149
+
150
+ async def execute(
151
+ self,
152
+ tool_call_id: str,
153
+ params: object,
154
+ signal: object = None,
155
+ on_update: object = None,
156
+ ) -> AgentToolResult:
157
+ del on_update
158
+ cancel = signal if isinstance(signal, CancelToken) else CancelToken()
159
+ outcome = await self._runner.run( # type: ignore[attr-defined]
160
+ ToolCall(id=tool_call_id, name=self.name, input=params), cancel
161
+ )
162
+ return _project_outcome(outcome.output, outcome.is_error)
163
+
164
+
165
+ def bridge_box_to_capabilities(box: ToolBox) -> list[AnyCapability]:
166
+ """Adapt every tool a mounted :class:`~indusagi.runtime.ToolBox`
167
+ advertises into a list of :data:`Capability` objects the conductor and
168
+ ledger consume.
169
+
170
+ Each box descriptor becomes one ``AgentTool``: its qualified name,
171
+ description, and parameter schema are carried verbatim, and its
172
+ ``execute`` defers to the box's shared ``runner``.
173
+
174
+ :param box: the tool box returned by ``mount_protocol_bridge``
175
+ """
176
+ runner = box.runner
177
+ return [
178
+ _BridgedCapability(
179
+ name=descriptor.name,
180
+ description=descriptor.description,
181
+ parameters=descriptor.parameters,
182
+ runner=runner,
183
+ )
184
+ for descriptor in box.descriptors()
185
+ ]
186
+
187
+
188
+ def bridge_capability_card(capability: AnyCapability) -> CapabilityCard:
189
+ """Re-present a grafted bridge capability as a :class:`CapabilityCard`.
190
+
191
+ Lets dynamic MCP tools appear in the same catalog/help surface as the
192
+ static cards: the card's ``build`` simply returns the already-live
193
+ capability (the graft happened at mount time, so there is nothing further
194
+ to construct). The owning server id is recovered from the qualified name
195
+ for the summary.
196
+
197
+ :param capability: a capability produced by
198
+ :func:`bridge_box_to_capabilities`
199
+ """
200
+ server = _server_of_qualified(capability.name)
201
+ summary = (
202
+ f'{capability.description} (bridged from MCP server "{server}")'
203
+ if server
204
+ else capability.description
205
+ )
206
+ return CapabilityCard(
207
+ id=capability_id(capability.name),
208
+ title=capability.label,
209
+ summary=summary,
210
+ build=lambda _ctx: capability,
211
+ )
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Enrollment: mount + fold into the ledger
216
+ # ---------------------------------------------------------------------------
217
+
218
+
219
+ @dataclass(frozen=True, slots=True, kw_only=True)
220
+ class AttachResult:
221
+ """The outcome of attaching one or more MCP servers to a ledger.
222
+
223
+ Bundles the new :class:`BridgeLedger` (with every reachable tool
224
+ enrolled), the live :class:`~indusagi.interop.ServerFleet` the caller owns
225
+ and must eventually tear down, the aggregate
226
+ :data:`~indusagi.interop.FleetStatus` for rendering, the count of tools
227
+ grafted, and a non-fatal :class:`DeckFault` when the mount itself failed
228
+ wholesale.
229
+ """
230
+
231
+ # The ledger after enrolling every grafted capability.
232
+ ledger: BridgeLedger
233
+ # How many remote tools were enrolled.
234
+ enrolled: int
235
+ # The running fleet of endpoints; caller closes it via `detach_bridge`.
236
+ fleet: ServerFleet | None = None
237
+ # Aggregate per-server health, when a fleet came up.
238
+ status: FleetStatus | None = None
239
+ # A wholesale-mount fault, when connecting the bridge threw.
240
+ fault: DeckFault | None = None
241
+
242
+
243
+ async def attach_bridge_capabilities(
244
+ ledger: BridgeLedger, config: BridgeConfig
245
+ ) -> AttachResult:
246
+ """Mount external MCP servers and enroll their tools into the bridge
247
+ ledger.
248
+
249
+ Connects every server in ``config`` through the framework's
250
+ ``mount_protocol_bridge``, adapts the resulting box into capabilities, and
251
+ folds each one into ``ledger`` as an ``enroll`` event — yielding a *new*
252
+ ledger (the input is untouched). The live fleet is returned so the caller
253
+ can read status and later tear it down; a faulted server simply
254
+ contributes no tools and is reflected in the status.
255
+
256
+ A wholesale failure (the mount call itself raising) is caught and reported
257
+ as a ``bridge`` :class:`DeckFault` on the result rather than raised, so a
258
+ bad MCP configuration degrades the deck instead of sinking session
259
+ bootstrap.
260
+
261
+ :param ledger: the ledger to enroll into (left untouched)
262
+ :param config: the set of MCP servers to connect and graft
263
+ """
264
+ try:
265
+ mounted = await _mount_protocol_bridge(config)
266
+ except Exception as cause:
267
+ message = (
268
+ f"bridge mount failed ({cause.kind}): {cause.message}"
269
+ if is_protocol_fault(cause)
270
+ else "bridge mount failed"
271
+ )
272
+ return AttachResult(
273
+ ledger=ledger,
274
+ enrolled=0,
275
+ fault=deck_fault("bridge", message, cause),
276
+ )
277
+
278
+ capabilities = bridge_box_to_capabilities(mounted.box)
279
+ next_ledger = ledger
280
+ for capability in capabilities:
281
+ next_ledger = enroll_bridge_card(
282
+ next_ledger,
283
+ EnrollRequest(
284
+ capability=capability,
285
+ server=_server_of_qualified(capability.name),
286
+ ),
287
+ )
288
+
289
+ return AttachResult(
290
+ ledger=next_ledger,
291
+ enrolled=len(capabilities),
292
+ fleet=mounted.fleet,
293
+ status=mounted.fleet.status(),
294
+ )
295
+
296
+
297
+ async def detach_bridge(
298
+ ledger: BridgeLedger,
299
+ fleet: ServerFleet,
300
+ servers: Sequence[str] | None = None,
301
+ ) -> BridgeLedger:
302
+ """Detach one or more servers: retire their ledger entries and close the
303
+ fleet.
304
+
305
+ Withdraws every named server's live capabilities from ``ledger`` (or, when
306
+ no names are given, every server present in the fleet status) and
307
+ gracefully tears the fleet down (the Python fleet's ``tear_down()``; TS
308
+ spelled it ``tearDown``). Returns the new ledger; the fleet is no longer
309
+ usable after this resolves. The teardown is best-effort — a close that
310
+ throws is swallowed so ledger withdrawal still completes.
311
+
312
+ :param ledger: the current ledger (left untouched)
313
+ :param fleet: the live fleet to close
314
+ :param servers: optional subset of server ids to detach; defaults to all
315
+ """
316
+ ids = list(servers) if servers is not None else list(fleet.status().keys())
317
+ next_ledger = ledger
318
+ for server in ids:
319
+ next_ledger = withdraw_server(next_ledger, server)
320
+ try:
321
+ await fleet.tear_down()
322
+ except Exception:
323
+ # Best-effort: the ledger already reflects the withdrawal.
324
+ pass
325
+ return next_ledger
326
+
327
+
328
+ def bridge_config(servers: Sequence[ServerConfig]) -> BridgeConfig:
329
+ """Convenience: turn a flat list of MCP server descriptions into the
330
+ :class:`~indusagi.interop.BridgeConfig` that
331
+ :func:`attach_bridge_capabilities` expects.
332
+
333
+ :param servers: the per-server transport configurations, in declaration
334
+ order
335
+ """
336
+ return BridgeConfig(servers=tuple(servers))
@@ -0,0 +1,358 @@
1
+ """Built-in capability bridge — ONE seam to the framework's native tools.
2
+
3
+ The framework (``indusagi.agent``, the actions surface) already authors its
4
+ file, search, shell, web, process, and checklist tools as fully-formed
5
+ ``AgentTool`` objects and ships a ``create_<name>_tool(cwd, options)`` factory
6
+ for each. This module is the single place the deck reaches across that seam:
7
+ it pairs every native factory with a deck :attr:`CapabilityCard.build`-shaped
8
+ closure so the whole framework tool set is re-exposed as :data:`Capability`
9
+ rows in one file rather than in a dozen one-line re-export stubs.
10
+
11
+ What this buys the rest of the deck:
12
+
13
+ - :data:`BUILTIN_BRIDGE` — a single mapping keyed by the wire-facing tool
14
+ name, each value a :data:`BridgeBuilder`-carrying descriptor that mints the
15
+ live capability for a :class:`DeckContext`. The manifest derives its catalog
16
+ rows from this mapping; nothing else hand-maintains a parallel list of
17
+ built-ins.
18
+ - :func:`build_builtin` — resolve-and-build one built-in by id, surfacing a
19
+ typed :class:`DeckFault` on an unknown id or a builder throw.
20
+ - :data:`BUILTIN_PROFILES` — the per-built-in profile membership the
21
+ data-driven provisioner intersects with a requested :data:`DeckProfile`, so
22
+ the survey (observe-only) set falls out of one table instead of a second
23
+ hand-written tool list.
24
+
25
+ Design notes (ported from TS ``src/capability-deck/builtin-bridge.ts``):
26
+
27
+ - The builders thread ``ctx.cwd`` (and, where a factory takes them, the
28
+ framework option bags) into the native factory. They never reimplement the
29
+ tool — the framework owns the read/edit/grep/bash/etc. behavior; the bridge
30
+ only *binds* it to a working context and re-labels it for the catalog.
31
+ - The process and checklist tools are stateful framework singletons exposed
32
+ through their factories; the bridge keeps them framework-backed while
33
+ presenting them as plain capabilities to the deck.
34
+ - All model-facing tool ``name`` strings are the framework's own wire contract
35
+ and are kept verbatim — the model's prompt and the conductor key off them.
36
+ Only the deck-side titles/summaries are the deck's own prose.
37
+
38
+ Port note — checklist wire names follow the *Python* framework
39
+ --------------------------------------------------------------
40
+ The TS lineage's framework named the checklist pair ``todoread`` /
41
+ ``todowrite``; the rebuilt Python framework's singletons advertise
42
+ ``todo_read`` / ``todo_set`` (verified live against ``indusagi.agent``).
43
+ Because the bridge's invariant is "ids are the framework wire contract,
44
+ verbatim", the descriptor ids here are ``todo_read`` / ``todo_set`` — the
45
+ names the model actually invokes — not the TS spellings.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ from collections.abc import Callable, Mapping
51
+ from dataclasses import dataclass
52
+ from types import MappingProxyType
53
+
54
+ from indusagi.agent import (
55
+ create_bash_tool,
56
+ create_edit_tool,
57
+ create_find_tool,
58
+ create_grep_tool,
59
+ create_ls_tool,
60
+ create_process_tool,
61
+ create_read_tool,
62
+ create_web_fetch_tool,
63
+ create_web_search_tool,
64
+ create_write_tool,
65
+ todo_read_tool,
66
+ todo_write_tool,
67
+ )
68
+
69
+ from .contract import (
70
+ AnyCapability,
71
+ Capability,
72
+ CapabilityId,
73
+ CardProfiles,
74
+ DeckContext,
75
+ DeckProfile,
76
+ capability_id,
77
+ deck_fault,
78
+ )
79
+
80
+ __all__ = [
81
+ "BUILTIN_BRIDGE",
82
+ "BUILTIN_IDS",
83
+ "BUILTIN_PROFILES",
84
+ "BridgeBuilder",
85
+ "BuiltinDescriptor",
86
+ "build_builtin",
87
+ "build_builtins_for_profile",
88
+ "builtin_descriptors",
89
+ ]
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Builder seam
94
+ # ---------------------------------------------------------------------------
95
+
96
+ #: A closure that mints one framework built-in :data:`Capability` for a
97
+ #: working context — the deck's view of a native factory after the bridge has
98
+ #: bound the ``cwd`` (and any option bag) the factory needs.
99
+ #:
100
+ #: Returns :data:`AnyCapability` because the heterogeneous built-ins carry
101
+ #: different parameter schemas; in Python the open and erased forms collapse
102
+ #: to the same structural ``AgentTool`` Protocol.
103
+ BridgeBuilder = Callable[[DeckContext], AnyCapability]
104
+
105
+
106
+ @dataclass(frozen=True, slots=True, kw_only=True)
107
+ class BuiltinDescriptor:
108
+ """One descriptor in the built-in catalog: the wire-facing id, the
109
+ deck-side title/summary prose, the profiles the tool participates in, and
110
+ the :data:`BridgeBuilder` that binds the framework factory to a context.
111
+
112
+ The manifest turns each of these into a :class:`CapabilityCard`; consumers
113
+ that want the raw built-in set read :data:`BUILTIN_BRIDGE` directly.
114
+ """
115
+
116
+ # Wire-facing tool name the model invokes (framework contract, verbatim).
117
+ id: CapabilityId
118
+ # Short human-facing title for catalogs and help text.
119
+ title: str
120
+ # One-line description in the deck's own voice.
121
+ summary: str
122
+ # Which deck profiles this built-in belongs to.
123
+ profiles: CardProfiles
124
+ # Bind the framework factory to a working context.
125
+ build: BridgeBuilder
126
+
127
+
128
+ # ---------------------------------------------------------------------------
129
+ # Bridge table
130
+ # ---------------------------------------------------------------------------
131
+
132
+ #: The two profiles that withhold mutation/shell — the observe-only set. A
133
+ #: built-in tagged with both ``authoring`` and ``survey`` is safe to expose
134
+ #: when the agent must not change the workspace; a built-in tagged
135
+ #: ``authoring`` only is a mutating or stateful tool that the survey profile
136
+ #: drops.
137
+ _READ_ONLY: CardProfiles = ("authoring", "survey")
138
+
139
+ #: Mutating / stateful tools: full-access (``authoring``) sessions only.
140
+ _MUTATING: CardProfiles = ("authoring",)
141
+
142
+ #: The single source of truth for the framework built-ins, in catalog order.
143
+ #:
144
+ #: Each row binds a wire name to its framework factory through a
145
+ #: :data:`BridgeBuilder`, declares its profile membership, and carries the
146
+ #: deck-side prose. This list collapses what would otherwise be a stub file
147
+ #: per tool into one table; the manifest's ``CAPABILITY_CARDS`` for built-ins
148
+ #: is a straight projection of it.
149
+ _BUILTIN_DESCRIPTORS: tuple[BuiltinDescriptor, ...] = (
150
+ BuiltinDescriptor(
151
+ id=capability_id("read"),
152
+ title="Read file",
153
+ summary=(
154
+ "Return the contents of a file at a path, optionally from an offset and "
155
+ "capped to a line limit; renders images inline where supported."
156
+ ),
157
+ profiles=_READ_ONLY,
158
+ build=lambda ctx: create_read_tool(ctx.cwd),
159
+ ),
160
+ BuiltinDescriptor(
161
+ id=capability_id("ls"),
162
+ title="List directory",
163
+ summary=(
164
+ "Enumerate the entries of a directory, with an optional cap on how many "
165
+ "are returned."
166
+ ),
167
+ profiles=_READ_ONLY,
168
+ build=lambda ctx: create_ls_tool(ctx.cwd),
169
+ ),
170
+ BuiltinDescriptor(
171
+ id=capability_id("grep"),
172
+ title="Search file contents",
173
+ summary=(
174
+ "Scan files for lines matching a pattern, with optional case-insensitivity, "
175
+ "literal matching, surrounding context, and a result cap."
176
+ ),
177
+ profiles=_READ_ONLY,
178
+ build=lambda ctx: create_grep_tool(ctx.cwd),
179
+ ),
180
+ BuiltinDescriptor(
181
+ id=capability_id("find"),
182
+ title="Find files by name",
183
+ summary=(
184
+ "Locate files and directories whose names match a glob-style pattern "
185
+ "beneath a root, capped to a result limit."
186
+ ),
187
+ profiles=_READ_ONLY,
188
+ build=lambda ctx: create_find_tool(ctx.cwd),
189
+ ),
190
+ BuiltinDescriptor(
191
+ id=capability_id("websearch"),
192
+ title="Web search",
193
+ summary=(
194
+ "Query the live web for a string and return a ranked set of result "
195
+ "snippets, capped to a requested count."
196
+ ),
197
+ profiles=_READ_ONLY,
198
+ build=lambda ctx: create_web_search_tool(),
199
+ ),
200
+ BuiltinDescriptor(
201
+ id=capability_id("webfetch"),
202
+ title="Fetch URL",
203
+ summary=(
204
+ "Retrieve a single URL and return its body as text, Markdown, or HTML, "
205
+ "with an optional request timeout."
206
+ ),
207
+ profiles=_READ_ONLY,
208
+ build=lambda ctx: create_web_fetch_tool(),
209
+ ),
210
+ BuiltinDescriptor(
211
+ id=capability_id("todo_read"),
212
+ title="Read checklist",
213
+ summary=(
214
+ "Read back the session's running task checklist so the agent can review "
215
+ "what is pending, active, or done."
216
+ ),
217
+ profiles=_READ_ONLY,
218
+ # The framework's default in-memory checklist singleton; a session-aware
219
+ # persistent store is wired by the separate checklist provisioner, not here.
220
+ build=lambda ctx: todo_read_tool,
221
+ ),
222
+ BuiltinDescriptor(
223
+ id=capability_id("write"),
224
+ title="Write file",
225
+ summary=(
226
+ "Create or overwrite a file at a path with the given contents, making "
227
+ "parent directories as needed."
228
+ ),
229
+ profiles=_MUTATING,
230
+ build=lambda ctx: create_write_tool(ctx.cwd),
231
+ ),
232
+ BuiltinDescriptor(
233
+ id=capability_id("edit"),
234
+ title="Edit file",
235
+ summary=(
236
+ "Apply a find-and-replace edit to a file, swapping an exact span of old "
237
+ "text for new text and reporting the resulting diff."
238
+ ),
239
+ profiles=_MUTATING,
240
+ build=lambda ctx: create_edit_tool(ctx.cwd),
241
+ ),
242
+ BuiltinDescriptor(
243
+ id=capability_id("bash"),
244
+ title="Run shell command",
245
+ summary=(
246
+ "Execute a shell command in the working directory, streaming its output "
247
+ "and honoring an optional timeout."
248
+ ),
249
+ profiles=_MUTATING,
250
+ build=lambda ctx: create_bash_tool(ctx.cwd),
251
+ ),
252
+ BuiltinDescriptor(
253
+ id=capability_id("process"),
254
+ title="Manage background processes",
255
+ summary=(
256
+ "Start, list, inspect, feed input to, and stop long-running background "
257
+ "commands without blocking the agent on them."
258
+ ),
259
+ profiles=_MUTATING,
260
+ # The TS factory took an option bag ({ cwd }); the Python factory
261
+ # accepts the same bag and reads `cwd` out of it.
262
+ build=lambda ctx: create_process_tool(options={"cwd": ctx.cwd}),
263
+ ),
264
+ BuiltinDescriptor(
265
+ id=capability_id("todo_set"),
266
+ title="Update checklist",
267
+ summary=(
268
+ "Replace or amend the session's task checklist, setting each item's "
269
+ "text, status, and priority."
270
+ ),
271
+ profiles=_MUTATING,
272
+ # Paired with the read singleton above; both share the framework default
273
+ # in-memory store. The checklist provisioner swaps in a persistent store.
274
+ build=lambda ctx: todo_write_tool,
275
+ ),
276
+ )
277
+
278
+ #: The framework built-ins keyed by their wire-facing :data:`CapabilityId` —
279
+ #: the single mapping the manifest and provisioner read from.
280
+ #:
281
+ #: Derived from the ordered descriptor tuple, so the keyed view and the
282
+ #: ordered list never drift: there is exactly one place a built-in is declared.
283
+ BUILTIN_BRIDGE: Mapping[str, BuiltinDescriptor] = MappingProxyType(
284
+ {descriptor.id: descriptor for descriptor in _BUILTIN_DESCRIPTORS}
285
+ )
286
+
287
+ #: The wire-facing ids of every framework built-in, in catalog order.
288
+ #: Convenient for ``--tools`` style selection and for the manifest's
289
+ #: projection.
290
+ BUILTIN_IDS: tuple[CapabilityId, ...] = tuple(
291
+ descriptor.id for descriptor in _BUILTIN_DESCRIPTORS
292
+ )
293
+
294
+ #: The profile membership of every built-in, keyed by id — the slice of the
295
+ #: profile table the data-driven provisioner reads to decide which built-ins a
296
+ #: requested :data:`DeckProfile` includes.
297
+ BUILTIN_PROFILES: Mapping[str, CardProfiles] = MappingProxyType(
298
+ {descriptor.id: descriptor.profiles for descriptor in _BUILTIN_DESCRIPTORS}
299
+ )
300
+
301
+
302
+ # ---------------------------------------------------------------------------
303
+ # Build helpers
304
+ # ---------------------------------------------------------------------------
305
+
306
+
307
+ def builtin_descriptors() -> tuple[BuiltinDescriptor, ...]:
308
+ """The ordered tuple of built-in descriptors — the manifest projects its
309
+ catalog rows from this, preserving order."""
310
+ return _BUILTIN_DESCRIPTORS
311
+
312
+
313
+ def build_builtin(id: CapabilityId, ctx: DeckContext) -> Capability:
314
+ """Resolve a built-in by id and build its live capability for a context.
315
+
316
+ Raises a typed :class:`DeckFault`: ``unknown_capability`` when the id is
317
+ not a framework built-in, or ``build_failed`` when the framework factory
318
+ throws. This is the sanctioned entry point so failure shaping stays
319
+ uniform across the deck.
320
+
321
+ :param id: the wire-facing built-in name to build
322
+ :param ctx: the working context to bind the factory to
323
+ """
324
+ descriptor = BUILTIN_BRIDGE.get(id)
325
+ if descriptor is None:
326
+ raise deck_fault(
327
+ "unknown_capability",
328
+ f'No framework built-in is registered under "{id}".',
329
+ )
330
+ try:
331
+ return descriptor.build(ctx)
332
+ except Exception as cause:
333
+ raise deck_fault(
334
+ "build_failed",
335
+ f'Framework built-in "{id}" failed to build.',
336
+ cause,
337
+ ) from cause
338
+
339
+
340
+ def build_builtins_for_profile(
341
+ profile: DeckProfile, ctx: DeckContext
342
+ ) -> list[AnyCapability]:
343
+ """Build every framework built-in eligible for a profile, bound to a
344
+ context.
345
+
346
+ Walks the bridge table once, keeps each built-in whose profile membership
347
+ admits the requested profile (``all`` admits everything), and binds it. A
348
+ single data-driven pass replaces a trio of per-profile build functions.
349
+
350
+ :param profile: the requested deck profile
351
+ :param ctx: the working context to bind each factory to
352
+ """
353
+ built: list[AnyCapability] = []
354
+ for descriptor in _BUILTIN_DESCRIPTORS:
355
+ if profile != "all" and profile not in descriptor.profiles:
356
+ continue
357
+ built.append(build_builtin(descriptor.id, ctx))
358
+ return built