indusagi-coding-agent 0.2.3 → 0.2.4

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 (31) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/entry.js +1733 -572
  3. package/dist/guardrails.js +21 -6
  4. package/dist/index.js +1687 -552
  5. package/dist/types/boot/heap.d.ts +31 -0
  6. package/dist/types/boot/runners/delegate-runner.d.ts +21 -1
  7. package/dist/types/boot/runners/server-mode.d.ts +71 -0
  8. package/dist/types/boot/runners/session.d.ts +24 -1
  9. package/dist/types/boot/runners/session.test.d.ts +6 -1
  10. package/dist/types/boot/server-token.d.ts +97 -0
  11. package/dist/types/capability-deck/contract.d.ts +7 -6
  12. package/dist/types/conductor/conductor.d.ts +24 -0
  13. package/dist/types/conductor/contract.d.ts +71 -7
  14. package/dist/types/conductor/index.d.ts +3 -3
  15. package/dist/types/conductor/permissions.d.ts +74 -4
  16. package/dist/types/conductor/post-edit-diagnostics.test.d.ts +8 -3
  17. package/dist/types/conductor/quota-error.d.ts +35 -0
  18. package/dist/types/console/auth-status.d.ts +28 -0
  19. package/dist/types/console/components/TerminalConsole.d.ts +0 -5
  20. package/dist/types/console/contract.d.ts +13 -0
  21. package/dist/types/console/input/index.d.ts +1 -1
  22. package/dist/types/console/input/keymap.d.ts +1 -1
  23. package/dist/types/console/input/paste.d.ts +53 -1
  24. package/dist/types/console/overlays/approval-queue.d.ts +18 -1
  25. package/dist/types/console/overlays/index.d.ts +1 -1
  26. package/dist/types/launch/login.d.ts +68 -0
  27. package/dist/types/launch/oauth.test.d.ts +20 -0
  28. package/dist/types/window-budget/summarize/condense.d.ts +6 -0
  29. package/dist/types/workflow-engine/agent-runner.d.ts +20 -1
  30. package/dist/types/workspace/brand.d.ts +1 -1
  31. package/package.json +2 -2
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Heap headroom guard.
3
+ *
4
+ * Node's default old-space cap (~2 GB on 64-bit builds) is too small for heavy
5
+ * runs — most acutely a burst of parallel sub-agents whose combined transcripts
6
+ * push the resident heap past the default and abort the whole process with a V8
7
+ * "JavaScript heap out of memory" fatal (the exact crash this guards against).
8
+ *
9
+ * `--max-old-space-size` can only be set at process launch, so before any real
10
+ * work begins we re-exec the *same* program once with a raised cap, inherit its
11
+ * stdio (so the interactive TUI runs unchanged in the child), and adopt its exit
12
+ * code. The re-exec is one level deep: the child carries `INDUS_HEAP_BOOSTED=1`
13
+ * so it skips the guard and runs the agent directly.
14
+ *
15
+ * Controlled by env:
16
+ * INDUS_MAX_HEAP_MB target old-space size in MB (default 4096; 0 disables
17
+ * the guard entirely and runs in-process).
18
+ * INDUS_HEAP_BOOSTED set internally on the child; never set this yourself.
19
+ *
20
+ * If the re-exec cannot spawn (a locked-down sandbox, a missing argv[1]) the
21
+ * guard silently falls back to running in the current, un-boosted process rather
22
+ * than failing to start.
23
+ */
24
+ /**
25
+ * Ensure the process has heap headroom, re-execing once if it does not.
26
+ *
27
+ * Returns normally in three cases: the guard is disabled, the cap is already
28
+ * raised, or the re-exec could not be spawned. Otherwise it does NOT return —
29
+ * it runs the boosted child to completion and exits with the child's status.
30
+ */
31
+ export declare function ensureHeapHeadroom(): void;
@@ -25,7 +25,7 @@
25
25
  * The `spawn`/`tools` options are pure test seams — they let a unit test drive
26
26
  * the runner with an in-memory fake instead of a real network round-trip.
27
27
  */
28
- import { type AgentMessage } from "../../conductor";
28
+ import { type AgentMessage, type SessionPermissionPolicy } from "../../conductor";
29
29
  import { type AgentTool } from "../../capability-deck";
30
30
  import type { DelegateRunner } from "../../capability-deck/cards/task-card";
31
31
  /**
@@ -75,6 +75,26 @@ export interface DelegateRunnerOptions {
75
75
  * `task` card — the recursion guard).
76
76
  */
77
77
  readonly tools?: () => AgentTool[];
78
+ /**
79
+ * Resolve the current server-tier gateway routing map (provider -> gateway
80
+ * base url). Called FRESH on every `run()` — never cached — so a mid-session
81
+ * `/login` (to "Indus Server"), including one that lands after this runner was
82
+ * constructed but before a given delegated call, is always picked up. This
83
+ * mirrors {@link getApiKey}, which is likewise invoked per-request rather than
84
+ * resolved once at construction.
85
+ */
86
+ readonly getGatewayBaseUrls?: () => Promise<Record<string, string>>;
87
+ /**
88
+ * The parent session's live permission policy: the SAME mutable rule list the
89
+ * parent gate reads plus a live getter onto the conductor's current mode. When
90
+ * present, every spawned sub-agent runs under a RESOLVER-LESS gate built from
91
+ * it — deny/ask rules, plan-mode enforcement, and the catastrophic-bash
92
+ * blocklist apply per inner tool call, and a mid-run mode switch (Shift+Tab)
93
+ * retargets the very next delegated tool call. A sub-agent cannot prompt, so
94
+ * an `ask` decision deterministically denies with an actionable message.
95
+ * Absent (a bare test construction), the sub-agent runs ungated as before.
96
+ */
97
+ readonly permissionPolicy?: SessionPermissionPolicy;
78
98
  }
79
99
  /**
80
100
  * Build a live {@link DelegateRunner} the host wires into the deck context.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Boot helper: decide whether a provider runs in **server mode** (Case B).
3
+ *
4
+ * A model turn takes one of three paths (see LOGIN_GATEWAY_PLAN.md §1):
5
+ * - **A. Local key** — a usable key for the provider is stored in the on-disk
6
+ * auth vault (or exported in the environment). The framework calls the
7
+ * provider directly; behaviour is unchanged and this module returns
8
+ * `{ serverMode: false }`.
9
+ * - **B. Server tier** — the provider is gateway-eligible, the user has **no**
10
+ * usable local key, and they hold a **valid server token**. The session token
11
+ * is vended as the provider "key" and the model is re-routed through the indus
12
+ * gateway. This module returns `{ serverMode: true, token }`.
13
+ * - **C. Blocked** — no local key and no valid token: `{ serverMode: false }`
14
+ * (the resolver then falls through to the framework's own env lookup, which
15
+ * surfaces the usual "no API key" guidance pointing at `indus login`).
16
+ *
17
+ * The two consumers in {@link file://./session.ts} call this with the SAME
18
+ * `(ctx, provider, vault)` triple so the key-resolver fallback and the
19
+ * gateway-base-url computation always agree on whether server mode is active.
20
+ */
21
+ import type { BootContext } from "../contract";
22
+ import type { AuthVault } from "../../launch/contract";
23
+ /**
24
+ * The outcome of {@link resolveServerMode}: whether the provider should route
25
+ * through the gateway this run, and (when so) the session token to hand back as
26
+ * the provider api key.
27
+ */
28
+ export interface ServerModeResult {
29
+ /** `true` => bind the gateway base url and use {@link token} as the key. */
30
+ serverMode: boolean;
31
+ /** The session token (`access_token`) to vend as the key, when `serverMode`. */
32
+ token?: string;
33
+ }
34
+ /**
35
+ * Whether `provider` may be routed through the indus gateway. Excludes the
36
+ * non-portable providers (vertex / bedrock / codex) and the deferred opencode
37
+ * `anthropic-messages` api per the §B.4 caveat.
38
+ */
39
+ export declare function isGatewayEligibleProvider(provider: string): boolean;
40
+ /** The server-tier provider slugs (for default-model selection / listing). */
41
+ export declare function serverTierProviders(): readonly string[];
42
+ /**
43
+ * Decide whether `provider` runs in server mode for this session.
44
+ *
45
+ * Server mode is active when ALL hold:
46
+ * 1. {@link isGatewayEligibleProvider}(provider) — excludes vertex/bedrock/codex
47
+ * and the deferred opencode `anthropic-messages` api (§B.4);
48
+ * 2. no EXPLICIT local key — the vault holds no usable key for the provider
49
+ * (an ambient env var is intentionally ignored for server-tier providers);
50
+ * 3. {@link hasValidServerToken}() — a stored, non-expired server token exists.
51
+ *
52
+ * Returns `{ serverMode: false }` whenever any condition fails, so the
53
+ * local-key (Case A) path is preserved byte-for-byte: the resolver then returns
54
+ * `undefined` and no gateway base url is bound.
55
+ *
56
+ * @param _ctx the boot context (reserved for future per-run policy; the vault is
57
+ * passed explicitly so callers share one instance)
58
+ * @param provider the framework provider slug to evaluate
59
+ * @param vault the on-disk credential vault to probe for a local key
60
+ */
61
+ export declare function resolveServerMode(_ctx: BootContext, provider: string, vault: AuthVault): Promise<ServerModeResult>;
62
+ /**
63
+ * Build the per-provider gateway base-url map for THIS session: for every
64
+ * gateway-eligible provider currently in server mode (no explicit local key +
65
+ * valid token), map `provider -> ${INDUS_SERVER_URL}/gateway/${provider}`.
66
+ *
67
+ * The conductor consults this map at EVERY model bind — including a runtime
68
+ * `/model` switch — so selecting ANY server-tier model (not just the one the
69
+ * session launched on) routes through the gateway. Empty when nothing qualifies.
70
+ */
71
+ export declare function resolveServerGatewayUrls(ctx: BootContext, vault: AuthVault): Promise<Record<string, string>>;
@@ -8,7 +8,7 @@
8
8
  * {@link ModelMatcher}; the conductor itself is built lazily so no framework agent
9
9
  * is constructed until the first turn runs.
10
10
  */
11
- import { type AgentTool, type SessionConductor } from "../../conductor";
11
+ import { type AgentTool, type ApprovalResolver, type CanUseToolFn, type PermissionMode, type PermissionRule, type SessionConductor } from "../../conductor";
12
12
  import { type MemoryStore } from "../../capability-deck";
13
13
  import { type DelegateRunner } from "../../capability-deck/cards/task-card";
14
14
  import type { WorkflowAgentRunner } from "../../workflow-engine";
@@ -54,6 +54,29 @@ export declare function composeSystem(tools: AgentTool[], inv: Invocation, cwd:
54
54
  * @param cwd the run's working directory
55
55
  */
56
56
  export declare function sessionScopeDir(sessionsRoot: string, cwd: string): string;
57
+ /**
58
+ * Build the `canUseTool` gate factory for a boot run.
59
+ *
60
+ * The factory receives a getter onto the conductor's LIVE permission mode plus the
61
+ * conductor's stable approval delegate, and returns the gate that reads them. The
62
+ * delegate is threaded straight into `requestApproval`: on a NON-interactive boot
63
+ * (oneshot / link) no resolver is ever installed, so the delegate denies and an
64
+ * `ask` decision deterministically denies with an actionable message. The
65
+ * interactive (React) console installs its overlay resolver via the conductor's
66
+ * `setApprovalResolver` AFTER mount, at which point the SAME delegate begins
67
+ * routing `ask` decisions to the overlay — no rebuild, fully additive.
68
+ *
69
+ * `rules` is the session's LIVE mutable rule list: the gate re-scans it on every
70
+ * call, and an `allow-always` approval appends into it (via `appendAllowRule`
71
+ * below), so the approval is remembered for the remainder of the session — and
72
+ * ONLY the session; nothing is ever written back to settings. Every gate the
73
+ * factory mints (and every sub-agent gate built over the same array) sees the
74
+ * appended rules immediately.
75
+ *
76
+ * Exported for the boot tests; product callers go through
77
+ * {@link buildSessionConductor}.
78
+ */
79
+ export declare function buildPermissionGate(rules: PermissionRule[], mode: PermissionMode, tools: readonly AgentTool[], planReachable?: boolean): ((currentMode: () => PermissionMode, requestApproval?: ApprovalResolver) => CanUseToolFn) | undefined;
57
80
  export declare function buildSessionConductor(ctx: BootContext): Promise<SessionConductor>;
58
81
  /**
59
82
  * The prompts a oneshot run submits: the positional first prompt when present,
@@ -1,10 +1,15 @@
1
1
  /**
2
- * Skills-surface wiring (#7 / fixes #11).
2
+ * Skills-surface wiring (#7 / fixes #11) + the boot permission-gate factory.
3
3
  *
4
4
  * Proves the chain that makes on-disk `SKILL.md` cards visible to the model:
5
5
  * `gatherModelSkills` walks `cwd/.indusagi/skills`, drops any card flagged
6
6
  * `disable-model-invocation`, and the survivors render into the briefing's
7
7
  * `<available_skills>` block via `composeSystem`. A `--system` override must NOT
8
8
  * carry the skills block (it replaces the whole prompt).
9
+ *
10
+ * `buildPermissionGate` is pinned here too: default mode ALWAYS builds a gate
11
+ * (only a rule-less bypass boot with plan unreachable goes gate-free), and the
12
+ * factory wires `appendAllowRule` into the live rule list so an `allow-always`
13
+ * approval is remembered for the remainder of the session (B2).
9
14
  */
10
15
  export {};
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Server-token store — the boot-layer persistence for the indus device-login
3
+ * session token. This is the app side of the login + model-gateway contract: a
4
+ * successful `indus login` device flow writes the better-auth session token
5
+ * here, and the key resolver later reads it back to drive "server mode" (Case B
6
+ * — no local provider key, but a valid server session, so requests are routed
7
+ * through the indus-server gateway with the session token as the api key).
8
+ *
9
+ * The token lives in a single JSON file under the app's profile directory
10
+ * (`~/.indusagi/agent/server-token.json`) — deliberately NOT the framework's
11
+ * `~/.better-auth` location, so the CLI session is isolated from any other
12
+ * tooling. As with the credential vault, the file is tiny and access is
13
+ * interactive, so we read / write the whole file each time and tolerate a
14
+ * missing or malformed file as "no token".
15
+ *
16
+ * Ported from supercli `server/src/lib/token.ts`; the load-bearing detail is the
17
+ * `expires_in` (seconds, from the device-flow response) → `expires_at` (absolute
18
+ * ISO timestamp, on disk) conversion, which lets {@link isServerTokenExpired}
19
+ * decide freshness without re-deriving the clock offset.
20
+ */
21
+ /**
22
+ * Root of the indus-server. Used to build the gateway base URL and the
23
+ * device-login endpoints. Any trailing slash(es) are stripped so callers can
24
+ * append paths with a single separator.
25
+ */
26
+ export declare const INDUS_SERVER_URL: string;
27
+ /** The app profile directory that holds the server session token. */
28
+ export declare const CONFIG_DIR: string;
29
+ /** Absolute path of the JSON file the session token is persisted to. */
30
+ export declare const TOKEN_FILE: string;
31
+ /** On-disk token shape (what {@link storeServerToken} writes / {@link getServerToken} returns). */
32
+ export interface TokenData {
33
+ /** The better-auth session token, used as the gateway api key in server mode. */
34
+ access_token: string;
35
+ /** Optional refresh token, when the device flow returns one. */
36
+ refresh_token?: string;
37
+ /** Token scheme; defaults to `"Bearer"`. */
38
+ token_type: string;
39
+ /** Optional granted scope string. */
40
+ scope?: string;
41
+ /** Absolute expiry as an ISO string, or null when the token has no expiry. */
42
+ expires_at: string | null;
43
+ /** When this record was written, as an ISO string. */
44
+ created_at: string;
45
+ }
46
+ /** Device-flow response shape accepted by {@link storeServerToken} (`expires_in` → `expires_at`). */
47
+ export interface ServerTokenInput {
48
+ /** The better-auth session token. */
49
+ access_token: string;
50
+ /** Optional refresh token. */
51
+ refresh_token?: string;
52
+ /** Token scheme; defaults to `"Bearer"` when omitted. */
53
+ token_type?: string;
54
+ /** Optional granted scope string. */
55
+ scope?: string;
56
+ /** Lifetime in seconds, relative to now; converted to an absolute `expires_at`. */
57
+ expires_in?: number;
58
+ }
59
+ /**
60
+ * Read the stored session token, tolerating a missing or malformed file as
61
+ * "no token".
62
+ *
63
+ * @returns the parsed {@link TokenData}, or `null` when nothing is stored.
64
+ */
65
+ export declare function getServerToken(): Promise<TokenData | null>;
66
+ /**
67
+ * Persist a session token from a device-flow response, converting the relative
68
+ * `expires_in` (seconds) into an absolute `expires_at` ISO timestamp and
69
+ * defaulting `token_type` to `"Bearer"`. Creates the profile directory if
70
+ * needed and writes the file with owner-only permissions.
71
+ *
72
+ * @param token the device-flow token payload.
73
+ * @returns `true` on success, `false` if the write failed.
74
+ */
75
+ export declare function storeServerToken(token: ServerTokenInput): Promise<boolean>;
76
+ /**
77
+ * Remove the stored session token (used by `indus logout`). A missing file is
78
+ * treated as nothing-to-do.
79
+ *
80
+ * @returns `true` when a file was deleted, `false` when none existed / removal failed.
81
+ */
82
+ export declare function clearServerToken(): Promise<boolean>;
83
+ /**
84
+ * Decide whether a token is expired. A token counts as expired when it is
85
+ * absent, has no `expires_at`, or has under 5 minutes of life remaining (the
86
+ * margin keeps a long turn from racing the expiry boundary).
87
+ *
88
+ * @param token the token to inspect (or `null`).
89
+ * @returns `true` when the token is missing or stale.
90
+ */
91
+ export declare function isServerTokenExpired(token: TokenData | null): boolean;
92
+ /**
93
+ * Convenience predicate: there is a stored token and it is not expired.
94
+ *
95
+ * @returns `true` when a usable (fresh) session token is on disk.
96
+ */
97
+ export declare function hasValidServerToken(): Promise<boolean>;
@@ -124,12 +124,13 @@ export type CardProfiles = readonly DeckProfile[];
124
124
  * The named capability sets a session can be provisioned with — the profile
125
125
  * table that replaces a trio of near-identical build functions.
126
126
  *
127
- * - `authoring` — the full mutating set: read/write/edit/ls + search + shell +
128
- * web + checklist + delegate + background processes + SaaS actions. The
129
- * default for an interactive coding session.
130
- * - `survey` — observe-only: read/ls/search/web/checklist. No filesystem
131
- * mutation and no shell. Safe when the agent must not change the workspace.
132
- * - `all` — every registered capability, nothing withheld.
127
+ * - `authoring` — the READ-ONLY built-in subset: read/ls/search/web/
128
+ * checklist-read. No filesystem mutation, no shell, no app-novel cards —
129
+ * the deck sub-agents (task/workflow) are provisioned with (see
130
+ * {@link "./provision"}'s PROFILE_TABLE, the authoritative mapping).
131
+ * - `survey` — every built-in (mutating ones included), no app-novel cards.
132
+ * - `all` — every registered capability, nothing withheld. What an
133
+ * interactive coding session runs with.
133
134
  */
134
135
  export type DeckProfile = "authoring" | "survey" | "all";
135
136
  /**
@@ -89,6 +89,18 @@ export interface CondenseOpts {
89
89
  readonly force?: boolean;
90
90
  readonly model?: Model<any>;
91
91
  readonly contextTokens?: number;
92
+ /**
93
+ * Cancellation for a manual `/compact`. A hook MAY honor it mid-summarization;
94
+ * the conductor always checks it after the hook returns and discards the
95
+ * result when aborted, so even a hook that ignores the signal cancels cleanly.
96
+ */
97
+ readonly signal?: AbortSignal;
98
+ /**
99
+ * The bound model's credential, resolved through the session's per-call key
100
+ * resolver. Forwarded so the summarizer authenticates exactly like a turn —
101
+ * server-tier sessions have no env-var fallback for this.
102
+ */
103
+ readonly apiKey?: string;
92
104
  }
93
105
  /**
94
106
  * The pluggable condense hook. Given the active branch's messages (and an options
@@ -186,4 +198,16 @@ export declare function reduceState(prev: ConductorState, action: StateAction):
186
198
  * @param deps injectable collaborators; all optional, live defaults supplied
187
199
  */
188
200
  export declare function createSessionConductor(options: SessionConductorOptions, deps?: ConductorDeps): SessionConductor;
201
+ /**
202
+ * Return the framework model unchanged, or — when a gateway base URL is set
203
+ * ("server mode") — a CLONE with its `baseUrl` swapped to the gateway so the
204
+ * request flows through the quota-enforcing indus-server. Never mutates the
205
+ * shared catalog card (always spreads into a fresh object). When
206
+ * `gatewayBaseUrl` is `undefined` the model is returned by reference, keeping
207
+ * the local-key path byte-for-byte unchanged.
208
+ */
209
+ export declare function withGatewayBaseUrl<M extends {
210
+ baseUrl: string;
211
+ provider: string;
212
+ }>(model: M, gatewayBaseUrls: Record<string, string> | undefined): M;
189
213
  export {};
@@ -103,8 +103,14 @@ export declare function conductorFault(kind: FaultKind, message: string, cause?:
103
103
  * - `tool_end` — a tool invocation finished (`ok` = no error).
104
104
  * - `turn_end` — the assistant turn settled; `usage` reports token spend.
105
105
  * - `persisted` — the latest node was committed to the transcript (`entryId`).
106
- * - `compacted` — the transcript was condensed to fit the context window.
107
- * - `fault` — a typed {@link ConductorFault} occurred.
106
+ * - `compacted` — the transcript WAS condensed (emitted on completion, not at
107
+ * the start). `manual` marks a user-driven `/compact` — a view may then
108
+ * reset its visible transcript — versus mid-turn auto-compaction, where the
109
+ * display must keep the in-flight exchange on screen.
110
+ * - `fault` — a typed {@link ConductorFault} occurred. `transient` marks a
111
+ * fault surfaced purely as an in-turn notice (e.g. a fallback-model swap on
112
+ * provider overload) — the turn keeps running, so a consumer must NOT treat
113
+ * it as the turn's end (must not clear a busy/in-flight indicator on it).
108
114
  * - `queue` — the pending-input queue changed; `count` is its new depth.
109
115
  * - `idle` — the conductor has no in-flight work and is ready for input.
110
116
  */
@@ -138,9 +144,11 @@ export type SessionSignal = {
138
144
  readonly entryId: string;
139
145
  } | {
140
146
  readonly kind: "compacted";
147
+ readonly manual?: boolean;
141
148
  } | {
142
149
  readonly kind: "fault";
143
150
  readonly fault: ConductorFault;
151
+ readonly transient?: boolean;
144
152
  } | {
145
153
  readonly kind: "queue";
146
154
  readonly count: number;
@@ -428,6 +436,18 @@ export interface SessionConductorOptions {
428
436
  * the swap entirely (behavior-preserving default).
429
437
  */
430
438
  readonly fallbackModelId?: string;
439
+ /**
440
+ * Per-provider indus-gateway base URLs ("server mode"), keyed by provider slug
441
+ * (e.g. `{ minimax: "http://host/gateway/minimax", sarvam: "…/gateway/sarvam" }`).
442
+ * Set by the session runner for every gateway-eligible provider the user has no
443
+ * local key for but holds a valid server token. At EVERY model bind (including a
444
+ * runtime `/model` switch) the conductor looks up the BOUND model's provider in
445
+ * this map and, when present, binds a CLONE of the framework model with its
446
+ * `baseUrl` swapped to that URL — so any server-tier model routes through the
447
+ * quota-enforcing server while the session token (via {@link getApiKey})
448
+ * authenticates it. A provider absent from the map keeps the direct path.
449
+ */
450
+ readonly gatewayBaseUrls?: Record<string, string>;
431
451
  /** Working directory the session is scoped to (defaults to process cwd). */
432
452
  readonly workspace?: string;
433
453
  /**
@@ -496,6 +516,11 @@ export interface SessionConductorOptions {
496
516
  */
497
517
  readonly checkpoint?: CheckpointPort;
498
518
  }
519
+ /**
520
+ * What a manual {@link SessionConductor.condense} run amounted to — the honest
521
+ * completion vocabulary `/compact` reports from (see the method doc).
522
+ */
523
+ export type CondenseOutcome = "condensed" | "nothing" | "cancelled" | "failed" | "busy";
499
524
  /**
500
525
  * The conductor of a single coding-agent session.
501
526
  *
@@ -537,6 +562,15 @@ export interface SessionConductor {
537
562
  pendingInputs(): readonly QueuedInput[];
538
563
  /** Discard every queued input. Emits a `{ kind: "queue" }` signal. */
539
564
  clearQueue(): void;
565
+ /**
566
+ * Promote the NEWEST queued input to run immediately: the in-flight turn (if
567
+ * any) is aborted — its work stops — while the rest of the queue is kept, and
568
+ * the promoted input runs as the very next turn. The user-facing "run my new
569
+ * message NOW" affordance for a long/stuck turn (issue #19), distinct from
570
+ * {@link abort} (which stops everything and clears the queue). Returns `false`
571
+ * when no input is queued.
572
+ */
573
+ steerNow(): boolean;
540
574
  /**
541
575
  * Remove and return the text of the most-recently queued input, or `undefined`
542
576
  * when the queue is empty. Lets a UI pop the last entry back into its prompt.
@@ -570,6 +604,20 @@ export interface SessionConductor {
570
604
  * @param id canonical id of the model to switch to
571
605
  */
572
606
  selectModel(id: string): void;
607
+ /**
608
+ * Replace the server-tier gateway routing map and immediately re-bind the
609
+ * currently selected model against it (without changing which model is
610
+ * selected). Lets an interactive mid-session sign-in (see `/login` ->
611
+ * "Indus Server") take effect right away: the gateway base-url map is
612
+ * otherwise frozen at conductor construction, so a login that happens after
613
+ * boot would silently never route the bound model through the gateway even
614
+ * though the per-request key resolver already started vending the fresh
615
+ * session token as the provider key.
616
+ *
617
+ * @param map provider id -> gateway base URL, as produced by
618
+ * `resolveServerGatewayUrls` for the current vault/token state
619
+ */
620
+ updateGatewayBaseUrls(map: Record<string, string>): void;
573
621
  /**
574
622
  * Replace the agent's tool deck for subsequent turns.
575
623
  *
@@ -584,11 +632,27 @@ export interface SessionConductor {
584
632
  */
585
633
  registerTools(tools: AgentTool[]): void;
586
634
  /**
587
- * Manually run the same transcript-condense path the auto-compactor uses,
588
- * emitting the existing `compacted` signal. Safe to call when idle; a no-op
589
- * when the condense hook returns the branch unchanged.
590
- */
591
- condense(): Promise<void>;
635
+ * Manually run the transcript-condense path (`/compact`) and report what
636
+ * happened, so the caller can give honest feedback instead of announcing
637
+ * "condensed" regardless:
638
+ * - `"condensed"` — the branch shrank and was rebound (emits `compacted`).
639
+ * - `"nothing"` — nothing older to fold (single-turn session, or already
640
+ * compacted); the transcript is untouched.
641
+ * - `"cancelled"` — {@link cancelCondense} fired mid-run; the digest was
642
+ * discarded and the transcript is untouched.
643
+ * - `"failed"` — the condense hook threw; a typed fault was emitted.
644
+ * - `"busy"` — a turn is in flight; compact after it settles (or abort
645
+ * it first).
646
+ * While the condense runs, {@link submit} queues instead of racing it, and the
647
+ * queue drains once the condense settles.
648
+ */
649
+ condense(): Promise<CondenseOutcome>;
650
+ /**
651
+ * Cancel an in-flight manual {@link condense} (the `/compact` Esc affordance).
652
+ * The summarizer's result is discarded and the transcript stays untouched; a
653
+ * no-op when no manual condense is running.
654
+ */
655
+ cancelCondense(): void;
592
656
  /**
593
657
  * Branch the transcript from a prior node. A new branch is opened whose parent
594
658
  * is `entryId`; the agent's message list is rebound to that branch's root→leaf
@@ -14,13 +14,13 @@
14
14
  * land; consumers import the conductor surface from `src/conductor` rather than
15
15
  * reaching into individual modules.
16
16
  */
17
- export type { FaultKind, ConductorFault, SessionSignal, SignalKind, SignalOf, SignalHandler, TranscriptSchema, TranscriptRole, TranscriptEntry, SessionHead, ModelCardRef, MatchQuery, ConductorPhase, ConductorState, QueueMode, QueuedInput, SessionStats, ExecuteBashOptions, BashOutcome, SessionConductor, SessionConductorOptions, AgentMessage, AgentTool, CanUseToolFn, ThinkingLevel, Model, Usage, KnownProvider, PermissionMode, } from "./contract";
17
+ export type { FaultKind, ConductorFault, SessionSignal, SignalKind, SignalOf, SignalHandler, TranscriptSchema, TranscriptRole, TranscriptEntry, SessionHead, ModelCardRef, MatchQuery, ConductorPhase, ConductorState, QueueMode, QueuedInput, SessionStats, ExecuteBashOptions, BashOutcome, SessionConductor, SessionConductorOptions, CondenseOutcome, AgentMessage, AgentTool, CanUseToolFn, ThinkingLevel, Model, Usage, KnownProvider, PermissionMode, } from "./contract";
18
18
  export { conductorFault, TRANSCRIPT_SCHEMA } from "./contract";
19
- export { createSessionConductor, reduceState, noopCondense, type AgentLike, type ConductorDeps, type CondenseFn, type RetryPolicy, } from "./conductor";
19
+ export { createSessionConductor, reduceState, noopCondense, withGatewayBaseUrl, type AgentLike, type ConductorDeps, type CondenseFn, type RetryPolicy, } from "./conductor";
20
20
  export { SignalHub, translateAgentEvent, type SignalHubOptions } from "./signal-hub";
21
21
  export { ModelCatalog, ModelMatcher, canonicalId, toCardRef, type CatalogCard, type CatalogSource, type ResolveInput, } from "./catalog";
22
22
  export { TranscriptStore, memoryBackend, fsBackend, replay, type TranscriptBackend, type TranscriptState, type TranscriptClock, type TranscriptStoreOptions, } from "./transcript-store";
23
23
  export { parseSkillInvocation, type SkillInvocation } from "./skill-parse";
24
- export { resolveRuleDecision, parseRule, makeRule, toolMatchesRule, createPermissionGate, isReadOnlyToolName, isEditToolName, READ_ONLY_TOOL_NAMES, EDIT_TOOL_NAMES, type PermissionBehavior, type PermissionRule, type PermissionDecision, type CanUseToolFn as PermissionCanUseToolFn, type ApprovalChoice, type ApprovalResolver, type PermissionGateConfig, } from "./permissions";
24
+ export { resolveRuleDecision, parseRule, makeRule, toolMatchesRule, createPermissionGate, createSubagentPermissionGate, collectReadOnlyToolNames, allowAlwaysRuleStrings, isReadOnlyToolName, isEditToolName, READ_ONLY_TOOL_NAMES, EDIT_TOOL_NAMES, type PermissionBehavior, type PermissionRule, type PermissionDecision, type CanUseToolFn as PermissionCanUseToolFn, type ApprovalChoice, type ApprovalOutcome, type ApprovalResolver, type PermissionGateConfig, type SessionPermissionPolicy, } from "./permissions";
25
25
  export { parseBashCommand, catastrophicReason, isCatastrophicCommand, evaluateCatastrophic, bashSubcommandSubjects, type ParsedCommand, } from "./bash-guard";
26
26
  export { DiagnosticsEngine, createDiagnosticKey, deduplicateAndCap, formatDiagnosticsSummary, getSeveritySymbol, parseTscOutput, parseEslintOutput, runTscDiagnostics, runEslintDiagnostics, hasTsConfig, hasEslintConfig, isTypeScriptOrJs, areDiagnosticsEqual, MAX_PER_FILE, MAX_TOTAL, MAX_SUMMARY_CHARS, type Diagnostic, type DiagnosticFile, type DiagnosticSeverity, type DiagnosticsConfig, type DiagnosticRunner, type DedupOptions, type Position, } from "./diagnostics";
@@ -74,16 +74,29 @@ export type CanUseToolFn = (toolName: string, input: unknown, opts: {
74
74
  * - `deny` — block this call.
75
75
  */
76
76
  export type ApprovalChoice = "allow-once" | "allow-always" | "deny";
77
+ /**
78
+ * What an {@link ApprovalResolver} may resolve to: a user's {@link ApprovalChoice},
79
+ * or `"unavailable"` — the conductor's stable delegate answers that when NO
80
+ * interactive resolver is installed behind it (a headless boot, or the console not
81
+ * yet mounted). The gate maps `"unavailable"` to the actionable "requires
82
+ * approval" deny (pointing at `--permission-mode` / settings allow rules) rather
83
+ * than the "denied by the user" message, which would be false when no user ever
84
+ * saw a prompt.
85
+ */
86
+ export type ApprovalOutcome = ApprovalChoice | "unavailable";
77
87
  /**
78
88
  * The OPTIONAL host approval resolver. When present, an `ask` decision awaits it;
79
89
  * the host (an interactive overlay) returns the user's choice. It MUST resolve to
80
90
  * `"deny"` on abort so a cancelled turn never hangs on a pending prompt. When the
81
91
  * resolver is absent (non-interactive boot / oneshot / link), an `ask` decision
82
- * deterministically denies.
92
+ * deterministically denies. A stable pass-through delegate (the conductor's)
93
+ * resolves `"unavailable"` while no real resolver is installed behind it, so the
94
+ * gate can surface the actionable non-interactive deny message instead of a
95
+ * fictitious user denial.
83
96
  */
84
97
  export type ApprovalResolver = (toolName: string, input: unknown, opts: {
85
98
  signal?: AbortSignal;
86
- }) => Promise<ApprovalChoice>;
99
+ }) => Promise<ApprovalOutcome>;
87
100
  /**
88
101
  * Tool names known to only inspect state (never mutate). Mirrors the framework's
89
102
  * `READ_ONLY_TOOL_NAMES`; matched case-insensitively. A tool the framework marks
@@ -172,6 +185,22 @@ export declare function toolMatchesRule(toolName: string, input: unknown, rule:
172
185
  * Pure and total: no I/O, no async, deterministic for a given input.
173
186
  */
174
187
  export declare function resolveRuleDecision(toolName: string, input: unknown, rules: readonly PermissionRule[], mode: PermissionMode, readOnlyExtra?: ReadonlySet<string>): PermissionBehavior;
188
+ /**
189
+ * The rule strings an `allow-always` approval mints — i.e. what the session will
190
+ * REMEMBER for this tool call.
191
+ *
192
+ * For the shell tool the remembered rules are scoped to the command: one
193
+ * `Bash(<sub-command>)` rule per constituent sub-command (so approving
194
+ * `git status && npm test` remembers both halves, and re-running either — or the
195
+ * same compound — auto-allows), but approving one command never whitelists the
196
+ * whole shell. Every other tool remembers the bare tool name (`Edit`), matching
197
+ * the prompt's "remember the tool for this session" copy.
198
+ *
199
+ * Exported so the approval overlay can show the user exactly what "Allow always"
200
+ * will remember (the `suggestions` line), and so the gate and the UI can never
201
+ * disagree about it.
202
+ */
203
+ export declare function allowAlwaysRuleStrings(toolName: string, input: unknown): string[];
175
204
  /** Configuration for {@link createPermissionGate}. */
176
205
  export interface PermissionGateConfig {
177
206
  /**
@@ -188,8 +217,11 @@ export interface PermissionGateConfig {
188
217
  readonly requestApproval?: ApprovalResolver;
189
218
  /**
190
219
  * Append a session-scoped allow rule when the host returns `allow-always`. The
191
- * conductor owns the mutable rule list and passes a setter here so the new rule
192
- * is visible to subsequent calls within the session.
220
+ * boot layer owns the mutable rule list (the SAME array instance `rules` refers
221
+ * to) and passes a push here, so the appended rule is visible to every
222
+ * subsequent gate consultation within the session — and to every other gate
223
+ * built over the same list (sub-agent gates included). Session-scoped only:
224
+ * the rule is never persisted to settings.
193
225
  */
194
226
  readonly appendAllowRule?: (rule: PermissionRule) => void;
195
227
  /**
@@ -215,3 +247,41 @@ export interface PermissionGateConfig {
215
247
  * The result is assignable to `indusagi/agent`'s `CanUseToolFn`.
216
248
  */
217
249
  export declare function createPermissionGate(config: PermissionGateConfig): CanUseToolFn;
250
+ /**
251
+ * The live permission policy a session shares with its sub-agent runners (the
252
+ * `task` and `workflow` tools): the SAME mutable rule list the parent gate reads
253
+ * — so a session-scoped `allow-always` rule and the settings deny/ask rules are
254
+ * enforced inside delegations too — plus a live getter onto the parent
255
+ * conductor's permission mode, so a mid-run Shift+Tab (bypass → default, plan,
256
+ * back to bypass, …) retargets the very next sub-agent tool call.
257
+ */
258
+ export interface SessionPermissionPolicy {
259
+ /** The session's ordered rule list — the live array, never a snapshot. */
260
+ readonly rules: readonly PermissionRule[];
261
+ /** Live getter onto the parent session's current permission mode. */
262
+ readonly mode: () => PermissionMode;
263
+ }
264
+ /**
265
+ * Collect the names of every tool flagged `readOnly: true` (structurally probed,
266
+ * so any deck/framework/MCP tool shape works). These names are auto-allowed by
267
+ * the gate in `default`/`acceptEdits`/`plan` beyond the static
268
+ * {@link READ_ONLY_TOOL_NAMES}.
269
+ */
270
+ export declare function collectReadOnlyToolNames(tools: ReadonlyArray<{
271
+ readonly name: string;
272
+ }>): ReadonlySet<string>;
273
+ /**
274
+ * Build the RESOLVER-LESS gate a sub-agent runs under.
275
+ *
276
+ * A sub-agent cannot prompt the user, so no {@link ApprovalResolver} is wired:
277
+ * an `ask` decision deterministically denies with an actionable message, while
278
+ * deny rules, plan-mode enforcement, the catastrophic-bash blocklist, read-only
279
+ * auto-allow, and bypass/acceptEdits behaviour all apply per inner tool call —
280
+ * against the LIVE parent mode, so mid-run mode switches reach delegations too.
281
+ *
282
+ * @param policy the session's shared rules + live mode getter
283
+ * @param tools the sub-agent's actual deck, probed for `readOnly: true` flags
284
+ */
285
+ export declare function createSubagentPermissionGate(policy: SessionPermissionPolicy, tools: ReadonlyArray<{
286
+ readonly name: string;
287
+ }>): CanUseToolFn;
@@ -6,8 +6,13 @@
6
6
  * over a scripted in-memory runner (no real tsc spawn). Verifies:
7
7
  * - a turn that introduces a NEW diagnostic enqueues the summary follow-up;
8
8
  * - a clean edit (no new diagnostics) enqueues nothing;
9
- * - re-editing a file re-surfaces a previously-cleared diagnostic;
10
- * - a non-TS edit never triggers the checker;
11
- * - the injected follow-up turn (which edits nothing) does not loop.
9
+ * - a still-unfixed diagnostic is delivered AT MOST ONCE per session — a
10
+ * follow-up turn whose "fix" edits fail to eliminate the same error must
11
+ * NOT be re-prompted with it (the endless self-continuation of issue #15);
12
+ * - even genuinely NEW errors per fix attempt stop re-prompting at the
13
+ * consecutive follow-up cap, and a fresh user prompt re-arms the budget;
14
+ * - `idle` is not emitted between a turn and its auto-drained follow-up (the
15
+ * spinner must not drop while the agent keeps working);
16
+ * - a non-TS edit never triggers the checker.
12
17
  */
13
18
  export {};