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,349 @@
1
+ """ToolInterceptor pipeline — the composable tool-call boundary.
2
+
3
+ This module is the runtime for the per-tool interception path. It replaces
4
+ the old two-part arrangement — a nested wrapper that re-bound a tool's
5
+ ``execute`` plus a separate before/after hook runner — with one ordered
6
+ pipeline of :class:`ToolInterceptor` stages folded around a single tool
7
+ execution.
8
+
9
+ The shape of a stage is ``enter`` (before the tool) and ``exit`` (after it),
10
+ either optional. The chain applies them as a reduce:
11
+
12
+ - **enter, forward.** Each matching stage's ``enter`` runs in registration
13
+ order. A stage may inspect the call, rewrite the decoded ``args`` (return
14
+ an :class:`ArgsRewrite`), or block the call outright (return a
15
+ :class:`GateDecision` with ``stop=True``). A block short-circuits: no later
16
+ ``enter`` runs, the real tool never executes, and the chain resolves with
17
+ ``blocked`` set.
18
+ - **execute, once.** With the final (possibly rewritten) args, the chain
19
+ invokes the real ``execute`` exactly once.
20
+ - **exit, reverse.** Each matching stage's ``exit`` runs in *reverse*
21
+ registration order, so the stage that entered first exits last — it wraps
22
+ the outermost layer around the call (classic onion ordering). A stage may
23
+ rewrite the result (return a replacement :class:`AgentToolResult`); the
24
+ rewritten result threads into the next (earlier-registered) stage's exit.
25
+
26
+ Fault isolation: a raise from any ``enter``/``exit`` stage is converted to an
27
+ :class:`AddonFault` routed to listeners and then treated as a no-op for that
28
+ stage — enter keeps the prior args (and does not block), exit keeps the prior
29
+ result. A stage's bug therefore degrades to "this stage did nothing" rather
30
+ than crashing the tool call. An error *from the real tool* is not swallowed:
31
+ it is offered to the ``exit`` stages (via :attr:`ToolExitContext.error`) and
32
+ then re-raised so the agent loop sees it, unless an exit stage produced a
33
+ replacement result (a deliberate recovery).
34
+
35
+ Matching: a stage's ``match`` is either an exact wire-facing tool name or
36
+ ``"*"`` (every tool). :meth:`AddonInterceptorChain.matches` answers whether
37
+ any stage applies to a tool, letting the host skip the pipeline entirely for
38
+ tools no addon touches.
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import inspect
44
+ from collections.abc import Callable, Mapping, Sequence
45
+ from typing import Any
46
+
47
+ from ..contract import (
48
+ AddonFault,
49
+ AddonFaultListener,
50
+ AddonId,
51
+ AgentToolResult,
52
+ ArgsRewrite,
53
+ ExecuteFn,
54
+ GateDecision,
55
+ InterceptResult,
56
+ InterceptorStage,
57
+ ToolEnterContext,
58
+ ToolExitContext,
59
+ ToolInterceptor,
60
+ addon_fault,
61
+ )
62
+
63
+ __all__ = [
64
+ "AddonInterceptorChain",
65
+ "interceptor",
66
+ ]
67
+
68
+
69
+ async def _maybe_await(value: Any) -> Any:
70
+ """Resolve a stage return that may be a plain value or an awaitable — the
71
+ Python analogue of TS ``await`` over ``T | Promise<T>``."""
72
+ if inspect.isawaitable(value):
73
+ return await value
74
+ return value
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Interceptor chain
79
+ # ---------------------------------------------------------------------------
80
+
81
+
82
+ class AddonInterceptorChain:
83
+ """The concrete :class:`InterceptorChain`: an ordered set of
84
+ :class:`ToolInterceptor` stages folded around one tool execution.
85
+
86
+ Build one with :meth:`from_interceptors` over the host's collected
87
+ interceptors (registration order preserved). Per tool call, :meth:`run`
88
+ selects the matching stages, applies their ``enter`` forward (honoring an
89
+ early block), executes the tool, then applies their ``exit`` in reverse.
90
+ Stage faults are routed to :meth:`on_fault` listeners and never crash the
91
+ call.
92
+ """
93
+
94
+ def __init__(self, interceptors: Sequence[ToolInterceptor]) -> None:
95
+ """
96
+ :param interceptors: every addon's recorded interceptors, in the order
97
+ the host folded them (registration order = enter order; reverse =
98
+ exit order)
99
+ """
100
+ # Every registered interceptor, in registration order.
101
+ self._interceptors: tuple[ToolInterceptor, ...] = tuple(interceptors)
102
+ # Live fault listeners, in registration order (dict = ordered set).
103
+ self._fault_listeners: dict[AddonFaultListener, None] = {}
104
+
105
+ @classmethod
106
+ def from_interceptors(
107
+ cls, interceptors: Sequence[ToolInterceptor]
108
+ ) -> AddonInterceptorChain:
109
+ """Build a chain from a flat list of interceptors.
110
+
111
+ A thin alias for the constructor that reads well at the host's fold
112
+ site (the TS static ``from``; renamed — ``from`` is a Python keyword).
113
+
114
+ :param interceptors: the folded interceptors across all addons
115
+ """
116
+ return cls(interceptors)
117
+
118
+ async def run(self, ctx: ToolEnterContext, execute: ExecuteFn) -> InterceptResult:
119
+ """Run a tool's execution through the interceptor pipeline.
120
+
121
+ Selects the stages whose ``match`` admits ``ctx.tool``, applies their
122
+ ``enter`` forward (rewriting args or short-circuiting on a block),
123
+ invokes ``execute`` once with the final args, then applies their
124
+ ``exit`` in reverse. A blocked enter resolves with ``blocked`` set
125
+ and never calls ``execute``. When no stage matches, ``execute`` runs
126
+ directly with the original args.
127
+
128
+ If the real tool raises, the error is offered to the matching
129
+ ``exit`` stages; an exit stage may return a replacement result to
130
+ recover, otherwise the error is re-raised to the caller after the
131
+ exit stages have observed it.
132
+
133
+ :param ctx: the entering call context (tool, call_id, initial args)
134
+ :param execute: the real tool invocation, called with the final args
135
+ """
136
+ matching = [
137
+ stage for stage in self._interceptors if _matches_tool(stage.match, ctx.tool)
138
+ ]
139
+ if len(matching) == 0:
140
+ return InterceptResult(result=await execute(ctx.args))
141
+
142
+ args, blocked = await self._apply_enter(ctx, matching)
143
+ if blocked is not None:
144
+ return InterceptResult(blocked=blocked)
145
+
146
+ exec_ctx = ToolEnterContext(tool=ctx.tool, call_id=ctx.call_id, args=args)
147
+ result: AgentToolResult | None = None
148
+ error: object | None = None
149
+ try:
150
+ result = await execute(exec_ctx.args)
151
+ except Exception as cause:
152
+ error = cause
153
+
154
+ exited = await self._apply_exit(exec_ctx, matching, result, error)
155
+ if exited is not None:
156
+ return InterceptResult(result=exited)
157
+ if error is not None:
158
+ # No exit stage recovered the failure — surface it to the agent loop.
159
+ raise error # type: ignore[misc]
160
+ # A matched call always produces a result here (execute resolved, no error).
161
+ return InterceptResult(result=result)
162
+
163
+ def matches(self, tool: str) -> bool:
164
+ """Whether any interceptor matches the given tool name."""
165
+ return any(_matches_tool(stage.match, tool) for stage in self._interceptors)
166
+
167
+ def on_fault(self, listener: AddonFaultListener) -> Callable[[], None]:
168
+ """Register a listener for per-stage :class:`AddonFault` reports.
169
+
170
+ Returns an idempotent unsubscribe thunk; registering the same
171
+ listener twice collapses to one slot.
172
+
173
+ :param listener: the fault sink
174
+ """
175
+ self._fault_listeners[listener] = None
176
+ active = True
177
+
178
+ def unsubscribe() -> None:
179
+ nonlocal active
180
+ if not active:
181
+ return
182
+ active = False
183
+ self._fault_listeners.pop(listener, None)
184
+
185
+ return unsubscribe
186
+
187
+ async def _apply_enter(
188
+ self,
189
+ ctx: ToolEnterContext,
190
+ stages: Sequence[ToolInterceptor],
191
+ ) -> tuple[Mapping[str, Any], GateDecision | None]:
192
+ """Apply the matching stages' ``enter`` in forward order, folding args
193
+ rewrites and short-circuiting on the first block. A stage that raises
194
+ is isolated and skipped (the prior args are kept, no block)."""
195
+ args = ctx.args
196
+ for stage in stages:
197
+ if stage.enter is None:
198
+ continue
199
+ here = ToolEnterContext(tool=ctx.tool, call_id=ctx.call_id, args=args)
200
+ try:
201
+ outcome = await _maybe_await(stage.enter(here))
202
+ except Exception as cause:
203
+ self._report(
204
+ addon_fault(
205
+ "handler",
206
+ f'Interceptor enter for "{ctx.tool}" threw.',
207
+ addon=stage.addon,
208
+ cause=cause,
209
+ )
210
+ )
211
+ continue
212
+ blocked, rewritten = _read_enter(outcome)
213
+ if blocked is not None:
214
+ return args, blocked
215
+ if rewritten is not None:
216
+ args = rewritten
217
+ return args, None
218
+
219
+ async def _apply_exit(
220
+ self,
221
+ ctx: ToolEnterContext,
222
+ stages: Sequence[ToolInterceptor],
223
+ result: AgentToolResult | None,
224
+ error: object | None,
225
+ ) -> AgentToolResult | None:
226
+ """Apply the matching stages' ``exit`` in reverse order, folding
227
+ result rewrites. Returns the rewritten result when any stage produced
228
+ one (or recovered an error), otherwise ``None`` (the caller keeps the
229
+ original result/error). A stage that raises is isolated and skipped."""
230
+ current = result
231
+ rewritten = False
232
+ for stage in reversed(stages):
233
+ if stage.exit is None:
234
+ continue
235
+ exit_ctx = _build_exit_context(ctx, current, error)
236
+ try:
237
+ outcome = await _maybe_await(stage.exit(exit_ctx))
238
+ except Exception as cause:
239
+ self._report(
240
+ addon_fault(
241
+ "handler",
242
+ f'Interceptor exit for "{ctx.tool}" threw.',
243
+ addon=stage.addon,
244
+ cause=cause,
245
+ )
246
+ )
247
+ continue
248
+ if outcome is not None:
249
+ current = outcome
250
+ rewritten = True
251
+ return current if rewritten else None
252
+
253
+ def _report(self, fault: AddonFault) -> None:
254
+ """Route a fault to every listener, guarding each listener against its
255
+ own raise."""
256
+ for listener in list(self._fault_listeners):
257
+ try:
258
+ listener(fault)
259
+ except Exception:
260
+ # A throwing fault listener must not break fan-out to the others.
261
+ pass
262
+
263
+
264
+ # ---------------------------------------------------------------------------
265
+ # Outcome reading
266
+ # ---------------------------------------------------------------------------
267
+
268
+
269
+ def _read_enter(
270
+ outcome: object,
271
+ ) -> tuple[GateDecision | None, Mapping[str, Any] | None]:
272
+ """Interpret an enter outcome into a normalized
273
+ ``(blocked, replacement_args)`` verdict — a block decision, a replacement
274
+ args mapping, or neither (continue unchanged).
275
+
276
+ Distinguishes the two contract shapes by type — a :class:`GateDecision`
277
+ carries ``stop``, an :class:`ArgsRewrite` carries ``args`` — and, for TS
278
+ structural-guard parity, also tolerates a plain mapping carrying a bool
279
+ ``stop`` key (gate) or a mapping-valued ``args`` key (rewrite). Anything
280
+ else is treated as "leave unchanged".
281
+ """
282
+ if outcome is None:
283
+ return None, None
284
+ if isinstance(outcome, GateDecision):
285
+ return (outcome if outcome.stop else None), None
286
+ if isinstance(outcome, ArgsRewrite):
287
+ return None, outcome.args
288
+ if isinstance(outcome, Mapping):
289
+ stop = outcome.get("stop")
290
+ if isinstance(stop, bool):
291
+ if not stop:
292
+ return None, None
293
+ raw_reason = outcome.get("reason")
294
+ reason = raw_reason if isinstance(raw_reason, str) else None
295
+ return GateDecision(stop=True, reason=reason), None
296
+ args = outcome.get("args")
297
+ if isinstance(args, Mapping):
298
+ return None, args
299
+ return None, None
300
+
301
+
302
+ def _build_exit_context(
303
+ ctx: ToolEnterContext,
304
+ result: AgentToolResult | None,
305
+ error: object | None,
306
+ ) -> ToolExitContext:
307
+ """Build the :class:`ToolExitContext` handed to an exit stage from the
308
+ entering context plus the current result/error. Only sets ``result`` when
309
+ present and ``error`` when present, matching the contract's "one of"
310
+ shape."""
311
+ if error is not None:
312
+ return ToolExitContext(tool=ctx.tool, call_id=ctx.call_id, error=error)
313
+ return ToolExitContext(tool=ctx.tool, call_id=ctx.call_id, result=result)
314
+
315
+
316
+ # ---------------------------------------------------------------------------
317
+ # Matching
318
+ # ---------------------------------------------------------------------------
319
+
320
+
321
+ def _matches_tool(match: str, tool: str) -> bool:
322
+ """Whether an interceptor's ``match`` (an exact name or ``"*"``) admits a
323
+ tool."""
324
+ return match == "*" or match == tool
325
+
326
+
327
+ # ---------------------------------------------------------------------------
328
+ # Construction helpers
329
+ # ---------------------------------------------------------------------------
330
+
331
+
332
+ def interceptor(
333
+ addon: AddonId,
334
+ match: str,
335
+ stage: InterceptorStage,
336
+ ) -> ToolInterceptor:
337
+ """Record one tool interceptor, stamping it with the matched tool and
338
+ owning addon — the shape the :class:`AddonSurface` produces and the chain
339
+ consumes.
340
+
341
+ Small convenience for hosts/tests assembling interceptors by hand without
342
+ going through the full surface builder. The supplied ``stage`` carries
343
+ only the enter/exit hooks; ``match`` and ``addon`` are filled here.
344
+
345
+ :param addon: the addon the interceptor is attributed to
346
+ :param match: the tool name to intercept, or ``"*"`` for every tool
347
+ :param stage: the enter/exit hooks
348
+ """
349
+ return ToolInterceptor(match=match, addon=addon, enter=stage.enter, exit=stage.exit)