indusagi-coding-agent 0.2.8 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/_decls/entry.ts +18 -0
- package/src/_decls/guardrails.ts +35 -0
- package/src/_decls/index.ts +26 -0
- package/src/addons/contract.ts +236 -0
- package/src/addons/dispatch/event-dispatcher.ts +164 -0
- package/src/addons/dispatch/index.ts +25 -0
- package/src/addons/dispatch/tool-interceptor.ts +208 -0
- package/src/addons/host.ts +225 -0
- package/src/addons/index.ts +112 -0
- package/src/addons/manifest.ts +158 -0
- package/src/addons/sandbox.ts +170 -0
- package/src/addons/surface.ts +78 -0
- package/src/boot/auth-vault.ts +195 -0
- package/src/boot/boot.ts +138 -0
- package/src/boot/contract.ts +238 -0
- package/src/boot/heap.ts +59 -0
- package/src/boot/index.ts +28 -0
- package/src/boot/invocation.ts +93 -0
- package/src/boot/runners/addon-wiring.ts +153 -0
- package/src/boot/runners/checkpoint.ts +169 -0
- package/src/boot/runners/delegate-runner.ts +294 -0
- package/src/boot/runners/index.ts +13 -0
- package/src/boot/runners/link-runner.ts +45 -0
- package/src/boot/runners/memdir.ts +168 -0
- package/src/boot/runners/oneshot-runner.ts +58 -0
- package/src/boot/runners/read-state.ts +90 -0
- package/src/boot/runners/registry.ts +42 -0
- package/src/boot/runners/repl-runner.ts +143 -0
- package/src/boot/runners/server-mode.ts +121 -0
- package/src/boot/runners/session.ts +641 -0
- package/src/boot/server-token.ts +148 -0
- package/src/boot/stages.ts +167 -0
- package/src/boot/upgrade/apply.ts +94 -0
- package/src/boot/upgrade/index.ts +13 -0
- package/src/boot/upgrade/upgrades.ts +289 -0
- package/src/briefing/compose.ts +150 -0
- package/src/briefing/context-docs.ts +19 -0
- package/src/briefing/contract.ts +717 -0
- package/src/briefing/index.ts +31 -0
- package/src/briefing/macros.ts +97 -0
- package/src/briefing/skills.ts +47 -0
- package/src/capability-deck/bridge-ledger/index.ts +27 -0
- package/src/capability-deck/bridge-ledger/key.ts +67 -0
- package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
- package/src/capability-deck/bridge-ledger/network.ts +117 -0
- package/src/capability-deck/builtin-bridge.ts +312 -0
- package/src/capability-deck/cards/bg-process-card.ts +335 -0
- package/src/capability-deck/cards/index.ts +115 -0
- package/src/capability-deck/cards/memory-card.ts +146 -0
- package/src/capability-deck/cards/plan-file.ts +97 -0
- package/src/capability-deck/cards/plan-tools.ts +185 -0
- package/src/capability-deck/cards/saas-card.ts +183 -0
- package/src/capability-deck/cards/task-card.ts +207 -0
- package/src/capability-deck/cards/todo-card.ts +168 -0
- package/src/capability-deck/cards/workflow-card.ts +247 -0
- package/src/capability-deck/contract.ts +388 -0
- package/src/capability-deck/index.ts +48 -0
- package/src/capability-deck/manifest.ts +109 -0
- package/src/capability-deck/provision.ts +169 -0
- package/src/channels/contract.ts +191 -0
- package/src/channels/framer.ts +50 -0
- package/src/channels/index.ts +101 -0
- package/src/channels/link/dialog.ts +129 -0
- package/src/channels/link/driver.ts +190 -0
- package/src/channels/link/index.ts +34 -0
- package/src/channels/link/server.ts +134 -0
- package/src/channels/oneshot.ts +90 -0
- package/src/channels/ops.ts +65 -0
- package/src/channels/session-ops.ts +81 -0
- package/src/conductor/bash-guard.ts +599 -0
- package/src/conductor/catalog/catalog.ts +116 -0
- package/src/conductor/catalog/index.ts +8 -0
- package/src/conductor/catalog/matcher.ts +134 -0
- package/src/conductor/conductor.ts +234 -0
- package/src/conductor/contract.ts +842 -0
- package/src/conductor/diagnostics.ts +227 -0
- package/src/conductor/index.ts +33 -0
- package/src/conductor/permissions.ts +588 -0
- package/src/conductor/quota-error.ts +49 -0
- package/src/conductor/signal-hub/hub.ts +46 -0
- package/src/conductor/signal-hub/index.ts +2 -0
- package/src/conductor/signal-hub/translate.test.ts +81 -0
- package/src/conductor/signal-hub/translate.ts +74 -0
- package/src/conductor/skill-parse/index.ts +2 -0
- package/src/conductor/skill-parse/parse.ts +108 -0
- package/src/conductor/transcript-store/index.ts +22 -0
- package/src/conductor/transcript-store/serialize.ts +116 -0
- package/src/conductor/transcript-store/store.ts +205 -0
- package/src/console/auth-status.ts +56 -0
- package/src/console/components/AgentsView.ts +165 -0
- package/src/console/components/BackgroundAgents.ts +155 -0
- package/src/console/components/Banner.ts +334 -0
- package/src/console/components/Composer.ts +94 -0
- package/src/console/components/StatusBar.ts +49 -0
- package/src/console/components/TerminalConsole.ts +1090 -0
- package/src/console/components/WorkingIndicator.ts +98 -0
- package/src/console/components/banner-sweep.ts +24 -0
- package/src/console/components/welcome.ts +74 -0
- package/src/console/contract.ts +630 -0
- package/src/console/index.ts +34 -0
- package/src/console/input/complete.ts +127 -0
- package/src/console/input/dir-reader.ts +34 -0
- package/src/console/input/index.ts +23 -0
- package/src/console/input/keymap.ts +159 -0
- package/src/console/input/paste.ts +104 -0
- package/src/console/mount.ts +56 -0
- package/src/console/overlays/approval-queue.ts +57 -0
- package/src/console/overlays/approval.ts +130 -0
- package/src/console/overlays/auth.ts +342 -0
- package/src/console/overlays/boards.ts +308 -0
- package/src/console/overlays/host.ts +36 -0
- package/src/console/overlays/index.ts +26 -0
- package/src/console/overlays/pickers.ts +258 -0
- package/src/console/overlays/sessions.ts +190 -0
- package/src/console/reducer.ts +182 -0
- package/src/console/slash/builtins.ts +81 -0
- package/src/console/slash/commands/dynamic.ts +83 -0
- package/src/console/slash/commands/integrations.ts +695 -0
- package/src/console/slash/commands/shared.ts +75 -0
- package/src/console/slash/commands/transcript.ts +263 -0
- package/src/console/slash/commands/workbench.ts +246 -0
- package/src/console/slash/index.ts +15 -0
- package/src/console/slash/registry.ts +70 -0
- package/src/console/slash/resolve.ts +63 -0
- package/src/console/startup.ts +209 -0
- package/src/console/theme/adapter.ts +45 -0
- package/src/console/theme/index.ts +7 -0
- package/src/console/theme/palette.ts +68 -0
- package/src/console/theme/resolve.ts +39 -0
- package/src/console/theme/tokens.ts +71 -0
- package/src/entry.ts +55 -0
- package/src/guardrails.ts +37 -0
- package/src/index.ts +18 -0
- package/src/insight/channel.ts +88 -0
- package/src/insight/contract.ts +185 -0
- package/src/insight/index.ts +110 -0
- package/src/insight/recorder.ts +213 -0
- package/src/insight/redaction.ts +157 -0
- package/src/insight/replay.ts +158 -0
- package/src/insight/sampling.ts +70 -0
- package/src/insight/serialize.ts +50 -0
- package/src/insight/sinks/console.ts +64 -0
- package/src/insight/sinks/file.ts +40 -0
- package/src/insight/sinks/index.ts +24 -0
- package/src/insight/sinks/stream.ts +54 -0
- package/src/integrations/sarvam/attach.ts +239 -0
- package/src/integrations/sarvam/config.ts +156 -0
- package/src/integrations/sarvam/index.ts +25 -0
- package/src/integrations/sarvam/sarvam.test.ts +60 -0
- package/src/integrations/sarvam/types.ts +27 -0
- package/src/integrations/zoho/attach.ts +342 -0
- package/src/integrations/zoho/config.ts +125 -0
- package/src/integrations/zoho/index.ts +27 -0
- package/src/integrations/zoho/types.ts +21 -0
- package/src/integrations/zoho/zoho.test.ts +50 -0
- package/src/kit/clipboard-image.ts +107 -0
- package/src/kit/external-editor.ts +48 -0
- package/src/kit/image.ts +59 -0
- package/src/kit/index.ts +51 -0
- package/src/kit/shell.ts +19 -0
- package/src/kit/tool-fetch.ts +85 -0
- package/src/launch/catalog.ts +148 -0
- package/src/launch/contract.ts +187 -0
- package/src/launch/credentials.ts +625 -0
- package/src/launch/index.ts +98 -0
- package/src/launch/invocation/attachments.ts +179 -0
- package/src/launch/invocation/flags.ts +196 -0
- package/src/launch/invocation/index.ts +25 -0
- package/src/launch/invocation/read.ts +260 -0
- package/src/launch/invocation/usage.ts +67 -0
- package/src/launch/login.ts +324 -0
- package/src/launch/oauth.test.ts +18 -0
- package/src/launch/oauth.ts +203 -0
- package/src/launch/packages.ts +194 -0
- package/src/launch/pickers.ts +189 -0
- package/src/runtime-bridge/bridges/_drive.ts +96 -0
- package/src/runtime-bridge/bridges/builtins.ts +68 -0
- package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
- package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
- package/src/runtime-bridge/bridges/index.ts +33 -0
- package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
- package/src/runtime-bridge/broker.ts +227 -0
- package/src/runtime-bridge/contract.ts +122 -0
- package/src/runtime-bridge/index.ts +79 -0
- package/src/runtime-bridge/sink.ts +180 -0
- package/src/sessions/contract.ts +81 -0
- package/src/sessions/index.ts +13 -0
- package/src/sessions/library.ts +229 -0
- package/src/settings/contract.ts +114 -0
- package/src/settings/index.ts +32 -0
- package/src/settings/manager.ts +117 -0
- package/src/transcript-export/index.ts +45 -0
- package/src/transcript-export/publish.ts +260 -0
- package/src/transcript-export/sgr.ts +315 -0
- package/src/transcript-export/template.ts +272 -0
- package/src/transcript-export/theme-bridge.ts +150 -0
- package/src/window-budget/budget/estimate.ts +135 -0
- package/src/window-budget/budget/gate.ts +33 -0
- package/src/window-budget/budget/index.ts +16 -0
- package/src/window-budget/budget/slice.ts +56 -0
- package/src/window-budget/condenser.ts +58 -0
- package/src/window-budget/contract.ts +184 -0
- package/src/window-budget/index.ts +19 -0
- package/src/window-budget/microcompact.ts +95 -0
- package/src/window-budget/rehydrate.ts +136 -0
- package/src/window-budget/summarize/condense.ts +103 -0
- package/src/window-budget/summarize/index.ts +14 -0
- package/src/window-budget/summarize/prompt.ts +149 -0
- package/src/workflow-engine/agent-runner.ts +181 -0
- package/src/workflow-engine/display.ts +224 -0
- package/src/workflow-engine/engine.ts +294 -0
- package/src/workflow-engine/index.ts +23 -0
- package/src/workflow-engine/parse.ts +172 -0
- package/src/workflow-engine/structured-output.ts +35 -0
- package/src/workspace/brand.ts +29 -0
- package/src/workspace/index.ts +18 -0
- package/src/workspace/locator.ts +103 -0
- package/src/workspace/runtime-detect.ts +64 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolInterceptor pipeline — the composable tool-call boundary.
|
|
3
|
+
*/
|
|
4
|
+
import type { AgentToolResult } from "indusagi/agent";
|
|
5
|
+
import {
|
|
6
|
+
addonFault,
|
|
7
|
+
type AddonFault,
|
|
8
|
+
type AddonFaultListener,
|
|
9
|
+
type AddonId,
|
|
10
|
+
type EnterOutcome,
|
|
11
|
+
type InterceptResult,
|
|
12
|
+
type InterceptorChain,
|
|
13
|
+
type ToolEnterContext,
|
|
14
|
+
type ToolInterceptor,
|
|
15
|
+
} from "../contract.js";
|
|
16
|
+
|
|
17
|
+
type Execute = (
|
|
18
|
+
args: Readonly<Record<string, unknown>>,
|
|
19
|
+
) => Promise<AgentToolResult<unknown>>;
|
|
20
|
+
|
|
21
|
+
interface EnterVerdict {
|
|
22
|
+
args?: Readonly<Record<string, unknown>>;
|
|
23
|
+
blocked?: { stop: boolean; reason?: string };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isGate(value: unknown): value is { stop: boolean; reason?: string } {
|
|
27
|
+
return (
|
|
28
|
+
typeof value === "object" &&
|
|
29
|
+
value !== null &&
|
|
30
|
+
"stop" in value &&
|
|
31
|
+
typeof (value as { stop: unknown }).stop === "boolean"
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isArgsRewrite(value: unknown): value is { args: Readonly<Record<string, unknown>> } {
|
|
36
|
+
return (
|
|
37
|
+
typeof value === "object" &&
|
|
38
|
+
value !== null &&
|
|
39
|
+
"args" in value &&
|
|
40
|
+
typeof (value as { args: unknown }).args === "object" &&
|
|
41
|
+
(value as { args: unknown }).args !== null
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readEnter(outcome: EnterOutcome): EnterVerdict {
|
|
46
|
+
if (outcome === undefined || outcome === null) return {};
|
|
47
|
+
if (isGate(outcome)) {
|
|
48
|
+
return outcome.stop ? { blocked: outcome } : {};
|
|
49
|
+
}
|
|
50
|
+
if (isArgsRewrite(outcome)) {
|
|
51
|
+
return { args: outcome.args };
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function matchesTool(match: string | "*", tool: string): boolean {
|
|
57
|
+
return match === "*" || match === tool;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildExitContext(
|
|
61
|
+
ctx: ToolEnterContext,
|
|
62
|
+
result: AgentToolResult<unknown> | undefined,
|
|
63
|
+
error: unknown,
|
|
64
|
+
): { tool: string; callId: string; result?: AgentToolResult<unknown>; error?: unknown } {
|
|
65
|
+
const base = { tool: ctx.tool, callId: ctx.callId };
|
|
66
|
+
if (error !== undefined) return { ...base, error };
|
|
67
|
+
return result !== undefined ? { ...base, result } : base;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class AddonInterceptorChain implements InterceptorChain {
|
|
71
|
+
private readonly interceptors: readonly ToolInterceptor[];
|
|
72
|
+
private readonly faultListeners: Set<AddonFaultListener>;
|
|
73
|
+
|
|
74
|
+
constructor(interceptors: readonly ToolInterceptor[]) {
|
|
75
|
+
this.interceptors = interceptors;
|
|
76
|
+
this.faultListeners = new Set();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static from(interceptors: readonly ToolInterceptor[]): AddonInterceptorChain {
|
|
80
|
+
return new AddonInterceptorChain(interceptors);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async run(ctx: ToolEnterContext, execute: Execute): Promise<InterceptResult> {
|
|
84
|
+
const matching = this.interceptors.filter((stage) => matchesTool(stage.match, ctx.tool));
|
|
85
|
+
if (matching.length === 0) {
|
|
86
|
+
return { result: await execute(ctx.args) };
|
|
87
|
+
}
|
|
88
|
+
const entered = await this.applyEnter(ctx, matching);
|
|
89
|
+
if (entered.blocked !== undefined) {
|
|
90
|
+
return { blocked: entered.blocked };
|
|
91
|
+
}
|
|
92
|
+
const execCtx = { ...ctx, args: entered.args ?? ctx.args };
|
|
93
|
+
let result: AgentToolResult<unknown> | undefined;
|
|
94
|
+
let error: unknown;
|
|
95
|
+
try {
|
|
96
|
+
result = await execute(execCtx.args);
|
|
97
|
+
} catch (cause) {
|
|
98
|
+
error = cause;
|
|
99
|
+
}
|
|
100
|
+
const exited = await this.applyExit(execCtx, matching, result, error);
|
|
101
|
+
if (exited !== undefined) {
|
|
102
|
+
return { result: exited };
|
|
103
|
+
}
|
|
104
|
+
if (error !== undefined) {
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
return { result };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
matches(tool: string): boolean {
|
|
111
|
+
return this.interceptors.some((stage) => matchesTool(stage.match, tool));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
onFault(listener: AddonFaultListener): () => void {
|
|
115
|
+
this.faultListeners.add(listener);
|
|
116
|
+
let active = true;
|
|
117
|
+
return () => {
|
|
118
|
+
if (!active) return;
|
|
119
|
+
active = false;
|
|
120
|
+
this.faultListeners.delete(listener);
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async applyEnter(
|
|
125
|
+
ctx: ToolEnterContext,
|
|
126
|
+
stages: readonly ToolInterceptor[],
|
|
127
|
+
): Promise<EnterVerdict> {
|
|
128
|
+
let args: Readonly<Record<string, unknown>> = ctx.args;
|
|
129
|
+
for (const stage of stages) {
|
|
130
|
+
if (stage.enter === undefined) continue;
|
|
131
|
+
const here = { ...ctx, args };
|
|
132
|
+
let outcome: EnterOutcome | undefined;
|
|
133
|
+
try {
|
|
134
|
+
outcome = await stage.enter(here);
|
|
135
|
+
} catch (cause) {
|
|
136
|
+
this.report(
|
|
137
|
+
addonFault("handler", `Interceptor enter for "${ctx.tool}" threw.`, {
|
|
138
|
+
addon: stage.addon,
|
|
139
|
+
cause,
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const verdict = readEnter(outcome);
|
|
145
|
+
if (verdict.blocked !== undefined) {
|
|
146
|
+
return { args, blocked: verdict.blocked };
|
|
147
|
+
}
|
|
148
|
+
if (verdict.args !== undefined) {
|
|
149
|
+
args = verdict.args;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { args };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async applyExit(
|
|
156
|
+
ctx: ToolEnterContext,
|
|
157
|
+
stages: readonly ToolInterceptor[],
|
|
158
|
+
result: AgentToolResult<unknown> | undefined,
|
|
159
|
+
error: unknown,
|
|
160
|
+
): Promise<AgentToolResult<unknown> | undefined> {
|
|
161
|
+
let current = result;
|
|
162
|
+
let rewritten = false;
|
|
163
|
+
for (let i = stages.length - 1; i >= 0; i--) {
|
|
164
|
+
const stage = stages[i];
|
|
165
|
+
if (stage.exit === undefined) continue;
|
|
166
|
+
const exitCtx = buildExitContext(ctx, current, error);
|
|
167
|
+
let outcome: AgentToolResult<unknown> | undefined;
|
|
168
|
+
try {
|
|
169
|
+
outcome = await stage.exit(exitCtx);
|
|
170
|
+
} catch (cause) {
|
|
171
|
+
this.report(
|
|
172
|
+
addonFault("handler", `Interceptor exit for "${ctx.tool}" threw.`, {
|
|
173
|
+
addon: stage.addon,
|
|
174
|
+
cause,
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (outcome !== undefined) {
|
|
180
|
+
current = outcome;
|
|
181
|
+
rewritten = true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return rewritten ? current : undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private report(fault: AddonFault): void {
|
|
188
|
+
for (const listener of [...this.faultListeners]) {
|
|
189
|
+
try {
|
|
190
|
+
listener(fault);
|
|
191
|
+
} catch {
|
|
192
|
+
/* ignore listener errors */
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Record one tool interceptor, stamping it with the matched tool and owning
|
|
200
|
+
* addon.
|
|
201
|
+
*/
|
|
202
|
+
export function interceptor(
|
|
203
|
+
addon: AddonId,
|
|
204
|
+
match: string | "*",
|
|
205
|
+
stage: Omit<ToolInterceptor, "match" | "addon">,
|
|
206
|
+
): ToolInterceptor {
|
|
207
|
+
return { ...stage, match, addon };
|
|
208
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AddonHost — the assembly point that turns a directory of addon modules into a
|
|
3
|
+
* single wired runtime.
|
|
4
|
+
*/
|
|
5
|
+
import {
|
|
6
|
+
addonFault,
|
|
7
|
+
addonId,
|
|
8
|
+
type AddonCommand,
|
|
9
|
+
type AddonDiscovery,
|
|
10
|
+
type AddonFault,
|
|
11
|
+
type AddonFaultListener,
|
|
12
|
+
type AddonId,
|
|
13
|
+
type AddonSource,
|
|
14
|
+
type AddonTool,
|
|
15
|
+
type EventDispatcher,
|
|
16
|
+
type EventSubscription,
|
|
17
|
+
type FrameworkHandles,
|
|
18
|
+
type InterceptorChain,
|
|
19
|
+
type ModuleLoader,
|
|
20
|
+
type RegisteredManifest,
|
|
21
|
+
type ToolInterceptor,
|
|
22
|
+
} from "./contract.js";
|
|
23
|
+
import { AddonEventDispatcher } from "./dispatch/event-dispatcher.js";
|
|
24
|
+
import { AddonInterceptorChain } from "./dispatch/tool-interceptor.js";
|
|
25
|
+
import { discoverSources } from "./manifest.js";
|
|
26
|
+
import { createJitiLoader } from "./sandbox.js";
|
|
27
|
+
import { createSurface } from "./surface.js";
|
|
28
|
+
|
|
29
|
+
export interface AddonHostDeps {
|
|
30
|
+
readonly loader?: ModuleLoader;
|
|
31
|
+
readonly handles?: FrameworkHandles;
|
|
32
|
+
readonly dispatcher?: (subscriptions: readonly EventSubscription[]) => EventDispatcher;
|
|
33
|
+
readonly chain?: (interceptors: readonly ToolInterceptor[]) => InterceptorChain;
|
|
34
|
+
readonly reservedActions?: Iterable<string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AddonRegistry {
|
|
38
|
+
readonly subscriptions: readonly EventSubscription[];
|
|
39
|
+
readonly interceptors: readonly ToolInterceptor[];
|
|
40
|
+
readonly commands: readonly AddonCommand[];
|
|
41
|
+
readonly tools: readonly AddonTool[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AddonSurfaceBundle {
|
|
45
|
+
readonly dispatch: EventDispatcher;
|
|
46
|
+
readonly interceptors: InterceptorChain;
|
|
47
|
+
readonly commands: readonly AddonCommand[];
|
|
48
|
+
readonly tools: readonly AddonTool[];
|
|
49
|
+
readonly loaded: readonly AddonId[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const RESERVED_ACTION_NAMES: ReadonlySet<string> = new Set([
|
|
53
|
+
"help",
|
|
54
|
+
"quit",
|
|
55
|
+
"exit",
|
|
56
|
+
"clear",
|
|
57
|
+
"model",
|
|
58
|
+
"compact",
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
export class AddonHost {
|
|
62
|
+
private readonly faultListeners: Set<AddonFaultListener> = new Set();
|
|
63
|
+
private readonly reserved: Set<string>;
|
|
64
|
+
private readonly subscriptions: EventSubscription[] = [];
|
|
65
|
+
private readonly interceptors: ToolInterceptor[] = [];
|
|
66
|
+
private readonly commands: AddonCommand[] = [];
|
|
67
|
+
private readonly tools: AddonTool[] = [];
|
|
68
|
+
private readonly claimedCommands: Set<string>;
|
|
69
|
+
private readonly claimedTools: Set<string> = new Set();
|
|
70
|
+
private readonly loaded: AddonId[] = [];
|
|
71
|
+
private readonly loader: ModuleLoader;
|
|
72
|
+
private readonly handles: FrameworkHandles;
|
|
73
|
+
private readonly buildDispatcher: (
|
|
74
|
+
subs: readonly EventSubscription[],
|
|
75
|
+
) => EventDispatcher;
|
|
76
|
+
private readonly buildChain: (interceptors: readonly ToolInterceptor[]) => InterceptorChain;
|
|
77
|
+
|
|
78
|
+
constructor(deps: AddonHostDeps = {}) {
|
|
79
|
+
this.loader = deps.loader ?? createJitiLoader();
|
|
80
|
+
this.handles = deps.handles ?? {};
|
|
81
|
+
this.buildDispatcher =
|
|
82
|
+
deps.dispatcher ?? ((subs) => AddonEventDispatcher.from(subs));
|
|
83
|
+
this.buildChain = deps.chain ?? ((stages) => AddonInterceptorChain.from(stages));
|
|
84
|
+
this.reserved =
|
|
85
|
+
deps.reservedActions === undefined
|
|
86
|
+
? new Set(RESERVED_ACTION_NAMES)
|
|
87
|
+
: new Set(deps.reservedActions);
|
|
88
|
+
this.claimedCommands = new Set(this.reserved);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
onFault(listener: AddonFaultListener): () => void {
|
|
92
|
+
this.faultListeners.add(listener);
|
|
93
|
+
let active = true;
|
|
94
|
+
return () => {
|
|
95
|
+
if (!active) return;
|
|
96
|
+
active = false;
|
|
97
|
+
this.faultListeners.delete(listener);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async loadAll(where: string | AddonDiscovery): Promise<AddonSurfaceBundle> {
|
|
102
|
+
const discovery = typeof where === "string" ? { workspace: where } : where;
|
|
103
|
+
const sources = discoverSources(discovery);
|
|
104
|
+
for (const source of sources) {
|
|
105
|
+
await this.loadOne(source);
|
|
106
|
+
}
|
|
107
|
+
return this.bundle();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async loadOne(source: AddonSource): Promise<void> {
|
|
111
|
+
let module;
|
|
112
|
+
try {
|
|
113
|
+
module = await this.loader.load(source.path);
|
|
114
|
+
} catch (cause) {
|
|
115
|
+
this.report(
|
|
116
|
+
addonFault("load", `Failed to load addon at ${source.path}.`, {
|
|
117
|
+
addon: source.id,
|
|
118
|
+
cause,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const id = module.id !== undefined ? addonId(module.id) : source.id;
|
|
124
|
+
const surface = createSurface(id, this.handles, module.version);
|
|
125
|
+
try {
|
|
126
|
+
await module.register(surface);
|
|
127
|
+
} catch (cause) {
|
|
128
|
+
this.report(
|
|
129
|
+
addonFault("register", `Addon "${id}" threw during register().`, {
|
|
130
|
+
addon: id,
|
|
131
|
+
cause,
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.fold(id, surface.manifest());
|
|
137
|
+
this.loaded.push(id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private bundle(): AddonSurfaceBundle {
|
|
141
|
+
const dispatch = this.buildDispatcher(this.subscriptions);
|
|
142
|
+
const interceptors = this.buildChain(this.interceptors);
|
|
143
|
+
this.wireFaults(dispatch);
|
|
144
|
+
this.wireFaults(interceptors);
|
|
145
|
+
return {
|
|
146
|
+
dispatch,
|
|
147
|
+
interceptors,
|
|
148
|
+
commands: [...this.commands],
|
|
149
|
+
tools: [...this.tools],
|
|
150
|
+
loaded: [...this.loaded],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get registry(): AddonRegistry {
|
|
155
|
+
return {
|
|
156
|
+
subscriptions: [...this.subscriptions],
|
|
157
|
+
interceptors: [...this.interceptors],
|
|
158
|
+
commands: [...this.commands],
|
|
159
|
+
tools: [...this.tools],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private fold(addon: AddonId, manifest: RegisteredManifest): void {
|
|
164
|
+
for (const sub of manifest.subscriptions) {
|
|
165
|
+
this.subscriptions.push(sub);
|
|
166
|
+
}
|
|
167
|
+
for (const stage of manifest.interceptors) {
|
|
168
|
+
this.interceptors.push(stage);
|
|
169
|
+
}
|
|
170
|
+
for (const command of manifest.commands) {
|
|
171
|
+
if (this.claimedCommands.has(command.name)) {
|
|
172
|
+
this.report(
|
|
173
|
+
addonFault(
|
|
174
|
+
"conflict",
|
|
175
|
+
this.reserved.has(command.name)
|
|
176
|
+
? `Addon "${addon}" command "${command.name}" is a reserved action name.`
|
|
177
|
+
: `Addon "${addon}" command "${command.name}" conflicts with an already-registered command.`,
|
|
178
|
+
{ addon },
|
|
179
|
+
),
|
|
180
|
+
);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
this.claimedCommands.add(command.name);
|
|
184
|
+
this.commands.push(command);
|
|
185
|
+
}
|
|
186
|
+
for (const tool of manifest.tools) {
|
|
187
|
+
if (this.claimedTools.has(tool.name)) {
|
|
188
|
+
this.report(
|
|
189
|
+
addonFault(
|
|
190
|
+
"conflict",
|
|
191
|
+
`Addon "${addon}" tool "${tool.name}" conflicts with an already-registered tool.`,
|
|
192
|
+
{ addon },
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
this.claimedTools.add(tool.name);
|
|
198
|
+
this.tools.push(tool);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private wireFaults(source: { onFault?: (listener: AddonFaultListener) => unknown }): void {
|
|
203
|
+
const onFault = source.onFault;
|
|
204
|
+
if (typeof onFault === "function") {
|
|
205
|
+
onFault.call(source, (fault: AddonFault) => this.report(fault));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private report(fault: AddonFault): void {
|
|
210
|
+
for (const listener of [...this.faultListeners]) {
|
|
211
|
+
try {
|
|
212
|
+
listener(fault);
|
|
213
|
+
} catch {
|
|
214
|
+
/* ignore listener errors */
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Construct an {@link AddonHost} with the given (all-optional) dependencies.
|
|
222
|
+
*/
|
|
223
|
+
export function createAddonHost(deps: AddonHostDeps = {}): AddonHost {
|
|
224
|
+
return new AddonHost(deps);
|
|
225
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
// Type declarations recovered from dist/types (no runtime body)
|
|
3
|
+
/**
|
|
4
|
+
* Addons subsystem — public barrel.
|
|
5
|
+
*
|
|
6
|
+
* Re-exports the FROZEN third-party customization contract: the
|
|
7
|
+
* {@link AddonManifest} an addon module returns and the {@link AddonSurface} its
|
|
8
|
+
* `register` entry point receives, the colon-named {@link HookEvent} taxonomy
|
|
9
|
+
* with its observe/transform/gate {@link HookHandler} middleware and the
|
|
10
|
+
* {@link EventDispatcher} that runs them, the tool-boundary {@link ToolInterceptor}
|
|
11
|
+
* stages and the {@link InterceptorChain} pipeline that folds them around a tool
|
|
12
|
+
* execution, the capabilities an addon contributes ({@link AddonTool},
|
|
13
|
+
* {@link AddonCommand}), the injectable {@link ModuleLoader} + {@link AddonDiscovery}
|
|
14
|
+
* loading seam, and the typed {@link AddonFault}.
|
|
15
|
+
*
|
|
16
|
+
* Behavior modules (the jiti-backed sandbox loader, the capability registry, the
|
|
17
|
+
* surface builder, the event dispatcher, and the interceptor chain) are added to
|
|
18
|
+
* this barrel as they land; consumers import the addon surface from
|
|
19
|
+
* `src/addons` rather than reaching into individual modules.
|
|
20
|
+
*/
|
|
21
|
+
export type {
|
|
22
|
+
BundledNamespace,
|
|
23
|
+
AddonId,
|
|
24
|
+
AddonManifest,
|
|
25
|
+
RegisteredManifest,
|
|
26
|
+
HookEvent,
|
|
27
|
+
HookKind,
|
|
28
|
+
GateDecision,
|
|
29
|
+
HookHandler,
|
|
30
|
+
EventSubscription,
|
|
31
|
+
EventDispatcher,
|
|
32
|
+
DispatchOutcome,
|
|
33
|
+
ToolEnterContext,
|
|
34
|
+
ToolExitContext,
|
|
35
|
+
EnterOutcome,
|
|
36
|
+
ExitOutcome,
|
|
37
|
+
ToolInterceptor,
|
|
38
|
+
InterceptorChain,
|
|
39
|
+
InterceptResult,
|
|
40
|
+
AddonTool,
|
|
41
|
+
CommandContext,
|
|
42
|
+
AddonCommand,
|
|
43
|
+
FrameworkHandles,
|
|
44
|
+
ExecOutcome,
|
|
45
|
+
AddonSurface,
|
|
46
|
+
ModuleLoader,
|
|
47
|
+
AddonDiscovery,
|
|
48
|
+
AddonSource,
|
|
49
|
+
AddonFaultKind,
|
|
50
|
+
AddonFault,
|
|
51
|
+
AddonFaultListener,
|
|
52
|
+
AgentMessage,
|
|
53
|
+
AgentTool,
|
|
54
|
+
AgentToolResult,
|
|
55
|
+
ThinkingLevel,
|
|
56
|
+
Model,
|
|
57
|
+
Component,
|
|
58
|
+
KeyId,
|
|
59
|
+
TSchema,
|
|
60
|
+
Static,
|
|
61
|
+
} from "./contract.js";
|
|
62
|
+
export {
|
|
63
|
+
BUNDLED_NAMESPACES,
|
|
64
|
+
ADDONS_DIR,
|
|
65
|
+
ADDON_MANIFEST_FIELD,
|
|
66
|
+
addonId,
|
|
67
|
+
emptyManifest,
|
|
68
|
+
addonFault,
|
|
69
|
+
} from "./contract.js";
|
|
70
|
+
/**
|
|
71
|
+
* Addon sandbox — the single jiti-backed {@link ModuleLoader}: bridges the
|
|
72
|
+
* framework {@link BUNDLED_NAMESPACES} into a loaded addon (virtual modules in a
|
|
73
|
+
* compiled binary, resolved aliases under Node) and turns a path into its
|
|
74
|
+
* {@link AddonManifest}. Injectable — tests supply a fake of the same interface.
|
|
75
|
+
*/
|
|
76
|
+
export {
|
|
77
|
+
createJitiLoader,
|
|
78
|
+
scrubInvisible,
|
|
79
|
+
expandPath,
|
|
80
|
+
resolvePath,
|
|
81
|
+
type JitiLoaderOptions,
|
|
82
|
+
} from "./sandbox.js";
|
|
83
|
+
/**
|
|
84
|
+
* Addon discovery — scan a `.indus/addons` directory for entry modules and fold
|
|
85
|
+
* an {@link AddonDiscovery} config into id-stamped {@link AddonSource}s.
|
|
86
|
+
*/
|
|
87
|
+
export { discoverAddons, discoverSources } from "./manifest.js";
|
|
88
|
+
/**
|
|
89
|
+
* Dispatch runtime — the unified {@link EventDispatcher} (observe / transform /
|
|
90
|
+
* gate) and the tool-boundary {@link InterceptorChain}, plus the `subscription`
|
|
91
|
+
* and `interceptor` minters for assembling their recorded shapes by hand.
|
|
92
|
+
*/
|
|
93
|
+
export { AddonEventDispatcher, AddonInterceptorChain, subscription, interceptor } from "./dispatch/index.js";
|
|
94
|
+
/**
|
|
95
|
+
* Addon surface — the per-addon recording {@link AddonSurface} the host hands to
|
|
96
|
+
* an addon's `register`; accumulates contributions and reads them back as a
|
|
97
|
+
* {@link RegisteredManifest}.
|
|
98
|
+
*/
|
|
99
|
+
export { createSurface } from "./surface.js";
|
|
100
|
+
/**
|
|
101
|
+
* Addon host — the assembly point: discover + load (injectable loader), register
|
|
102
|
+
* each addon against a fresh surface, fold every {@link RegisteredManifest} into
|
|
103
|
+
* one registry, and wire the dispatch engine + interceptor chain + collected
|
|
104
|
+
* commands/tools.
|
|
105
|
+
*/
|
|
106
|
+
export {
|
|
107
|
+
createAddonHost,
|
|
108
|
+
AddonHost,
|
|
109
|
+
type AddonHostDeps,
|
|
110
|
+
type AddonRegistry,
|
|
111
|
+
type AddonSurfaceBundle,
|
|
112
|
+
} from "./host.js";
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Addon discovery — scanning a workspace for addon entry modules.
|
|
3
|
+
*
|
|
4
|
+
* Where the sandbox module answers "how do I load *this* path", this module
|
|
5
|
+
* answers "*which* paths are there to load".
|
|
6
|
+
*/
|
|
7
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
8
|
+
import { basename, extname, isAbsolute, join, resolve } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
ADDON_MANIFEST_FIELD,
|
|
11
|
+
ADDONS_DIR,
|
|
12
|
+
addonId,
|
|
13
|
+
type AddonDiscovery,
|
|
14
|
+
type AddonSource,
|
|
15
|
+
} from "./contract.js";
|
|
16
|
+
|
|
17
|
+
const ENTRY_EXTENSIONS = [
|
|
18
|
+
".ts",
|
|
19
|
+
".tsx",
|
|
20
|
+
".mts",
|
|
21
|
+
".cts",
|
|
22
|
+
".js",
|
|
23
|
+
".mjs",
|
|
24
|
+
".cjs",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const INDEX_BASENAMES = ["index"];
|
|
28
|
+
|
|
29
|
+
function isHidden(name: string): boolean {
|
|
30
|
+
return name.startsWith(".");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hasEntryExtension(name: string): boolean {
|
|
34
|
+
return (ENTRY_EXTENSIONS as readonly string[]).includes(extname(name).toLowerCase());
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function entryKind(path: string): "directory" | "file" | undefined {
|
|
38
|
+
try {
|
|
39
|
+
const stat = statSync(path);
|
|
40
|
+
if (stat.isDirectory()) return "directory";
|
|
41
|
+
if (stat.isFile()) return "file";
|
|
42
|
+
return undefined;
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function listNames(dir: string): string[] {
|
|
49
|
+
try {
|
|
50
|
+
return readdirSync(dir);
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readManifestEntry(packageDir: string): string | undefined {
|
|
57
|
+
const manifestPath = join(packageDir, "package.json");
|
|
58
|
+
if (entryKind(manifestPath) !== "file") {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<string, unknown>;
|
|
63
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
const field = parsed[ADDON_MANIFEST_FIELD];
|
|
67
|
+
return typeof field === "string" ? field : undefined;
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolvePackageEntry(packageDir: string): string | undefined {
|
|
74
|
+
const declared = readManifestEntry(packageDir);
|
|
75
|
+
if (declared !== undefined) {
|
|
76
|
+
const declaredPath = resolve(packageDir, declared);
|
|
77
|
+
if (entryKind(declaredPath) === "file") {
|
|
78
|
+
return declaredPath;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const base of INDEX_BASENAMES) {
|
|
82
|
+
for (const ext of ENTRY_EXTENSIONS) {
|
|
83
|
+
const candidate = join(packageDir, `${base}${ext}`);
|
|
84
|
+
if (entryKind(candidate) === "file") {
|
|
85
|
+
return candidate;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function resolveTopLevelEntry(dir: string, name: string): string | undefined {
|
|
93
|
+
if (isHidden(name)) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const path = join(dir, name);
|
|
97
|
+
const kind = entryKind(path);
|
|
98
|
+
if (kind === "file") {
|
|
99
|
+
return hasEntryExtension(name) ? path : undefined;
|
|
100
|
+
}
|
|
101
|
+
if (kind === "directory") {
|
|
102
|
+
return resolvePackageEntry(path);
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Scan one addons directory and return the absolute entry-module paths it holds.
|
|
109
|
+
*/
|
|
110
|
+
export function discoverAddons(dir: string): string[] {
|
|
111
|
+
const entries: string[] = [];
|
|
112
|
+
for (const name of listNames(dir)) {
|
|
113
|
+
const entry = resolveTopLevelEntry(dir, name);
|
|
114
|
+
if (entry !== undefined) {
|
|
115
|
+
entries.push(entry);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return entries.sort();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function deriveAddonId(entryPath: string): string {
|
|
122
|
+
const file = basename(entryPath);
|
|
123
|
+
const stem = file.slice(0, file.length - extname(file).length);
|
|
124
|
+
const label =
|
|
125
|
+
(INDEX_BASENAMES as readonly string[]).includes(stem)
|
|
126
|
+
? basename(join(entryPath, ".."))
|
|
127
|
+
: stem;
|
|
128
|
+
return addonId(label.length > 0 ? label : entryPath);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function addonsDirFor(workspace: string, dir: string = ADDONS_DIR): string {
|
|
132
|
+
return resolve(workspace, dir);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Fold an {@link AddonDiscovery} config into the ordered, deduplicated list of
|
|
137
|
+
* {@link AddonSource}s.
|
|
138
|
+
*/
|
|
139
|
+
export function discoverSources(discovery: AddonDiscovery): AddonSource[] {
|
|
140
|
+
const paths: string[] = [];
|
|
141
|
+
if (discovery.workspace !== undefined) {
|
|
142
|
+
const dir = addonsDirFor(discovery.workspace, discovery.dir ?? ADDONS_DIR);
|
|
143
|
+
paths.push(...discoverAddons(dir));
|
|
144
|
+
}
|
|
145
|
+
for (const explicit of discovery.explicitPaths ?? []) {
|
|
146
|
+
paths.push(isAbsolute(explicit) ? explicit : resolve(explicit));
|
|
147
|
+
}
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
const sources: AddonSource[] = [];
|
|
150
|
+
for (const path of paths) {
|
|
151
|
+
if (seen.has(path)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
seen.add(path);
|
|
155
|
+
sources.push({ id: deriveAddonId(path), path });
|
|
156
|
+
}
|
|
157
|
+
return sources;
|
|
158
|
+
}
|