indusagi-coding-agent 0.2.12 → 0.2.14

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 (81) hide show
  1. package/.env.review.example +25 -0
  2. package/README.md +30 -0
  3. package/REVIEW_AGENT.md +137 -0
  4. package/dist/entry.js +1861 -979
  5. package/dist/guardrails.js +212 -36
  6. package/dist/index.js +2829 -1057
  7. package/dist/review-agent.js +4461 -0
  8. package/dist/review-server.js +4633 -0
  9. package/package.json +24 -7
  10. package/src/boot/auth-vault.ts +4 -5
  11. package/src/boot/contract.ts +1 -1
  12. package/src/boot/runners/addon-wiring.ts +111 -3
  13. package/src/boot/runners/benchmark-integrity.ts +107 -0
  14. package/src/boot/runners/repl-runner.ts +2 -1
  15. package/src/boot/runners/session.ts +26 -12
  16. package/src/boot/server-token.ts +20 -11
  17. package/src/boot/stages.ts +3 -6
  18. package/src/briefing/context-docs.ts +144 -12
  19. package/src/channels/oneshot.ts +6 -0
  20. package/src/conductor/catalog/catalog.ts +3 -1
  21. package/src/conductor/conductor.ts +77 -17
  22. package/src/conductor/contract.ts +9 -7
  23. package/src/conductor/index.ts +1 -1
  24. package/src/conductor/transcript-store/store.ts +4 -8
  25. package/src/console/auth-status.ts +9 -4
  26. package/src/console/components/AgentsView.ts +2 -2
  27. package/src/console/components/BackgroundAgents.ts +15 -2
  28. package/src/console/components/Banner.ts +20 -9
  29. package/src/console/components/Composer.ts +57 -82
  30. package/src/console/components/ScrollViewport.ts +46 -0
  31. package/src/console/components/TerminalConsole.ts +165 -162
  32. package/src/console/components/WorkingIndicator.ts +6 -5
  33. package/src/console/components/banner-sweep.ts +5 -1
  34. package/src/console/components/welcome.ts +3 -3
  35. package/src/console/contract.ts +10 -5
  36. package/src/console/index.ts +7 -7
  37. package/src/console/input/complete.ts +2 -2
  38. package/src/console/input/dir-reader.ts +3 -1
  39. package/src/console/input/graphemes.ts +22 -0
  40. package/src/console/input/keymap.ts +6 -4
  41. package/src/console/mount.ts +45 -3
  42. package/src/console/overlays/auth.ts +28 -8
  43. package/src/console/overlays/boards.ts +13 -40
  44. package/src/console/overlays/host.ts +10 -0
  45. package/src/console/overlays/pickers.ts +35 -30
  46. package/src/console/reducer.ts +12 -5
  47. package/src/console/slash/builtins.ts +4 -3
  48. package/src/console/slash/commands/integrations.ts +13 -14
  49. package/src/console/slash/commands/transcript.ts +3 -8
  50. package/src/console/slash/commands/workbench.ts +4 -4
  51. package/src/console/test-support.ts +48 -0
  52. package/src/index.ts +1 -0
  53. package/src/integrations/sarvam/config.ts +15 -13
  54. package/src/kit/external-editor.ts +17 -1
  55. package/src/kit/run-shell.ts +31 -0
  56. package/src/launch/catalog.ts +4 -2
  57. package/src/launch/contract.ts +1 -0
  58. package/src/launch/invocation/flags.ts +13 -1
  59. package/src/launch/invocation/read.ts +4 -3
  60. package/src/launch/login.ts +23 -10
  61. package/src/launch/oauth.ts +3 -0
  62. package/src/models.ts +166 -0
  63. package/src/review-agent/config.ts +75 -0
  64. package/src/review-agent/contract.ts +59 -0
  65. package/src/review-agent/github.ts +242 -0
  66. package/src/review-agent/index.ts +8 -0
  67. package/src/review-agent/prompt.ts +132 -0
  68. package/src/review-agent/reviewer.ts +118 -0
  69. package/src/review-agent/server.ts +221 -0
  70. package/src/review-agent/service.ts +165 -0
  71. package/src/review-agent/supabase.ts +209 -0
  72. package/src/review-agent/webhook.ts +76 -0
  73. package/src/security/private-state.ts +122 -0
  74. package/src/window-budget/summarize/condense.ts +4 -1
  75. package/src/workspace/brand.ts +1 -1
  76. package/src/workspace/locator.ts +2 -2
  77. package/supabase/migrations/20260827031803_indusagi_review_agent.sql +109 -0
  78. package/supabase/migrations/20260827035747_atomic_webhook_enqueue.sql +75 -0
  79. package/supabase/migrations/20260827040104_check_run_reconciliation.sql +8 -0
  80. package/supabase/migrations/20260827041049_owned_job_leases.sql +80 -0
  81. package/supabase/migrations/20260827042500_static_token_webhooks.sql +75 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "indusagi-coding-agent",
3
- "version": "0.2.12",
3
+ "version": "0.2.14",
4
4
  "description": "Indusagi coding agent — a terminal-first AI coding agent, built from scratch on the indusagi framework.",
5
5
  "author": "Varun Israni",
6
6
  "license": "AGPL-3.0",
@@ -24,7 +24,8 @@
24
24
  "main": "./dist/index.js",
25
25
  "bin": {
26
26
  "indus": "dist/entry.js",
27
- "indusagi": "dist/entry.js"
27
+ "indusagi": "dist/entry.js",
28
+ "indusagi-review": "dist/review-server.js"
28
29
  },
29
30
  "exports": {
30
31
  ".": {
@@ -33,35 +34,46 @@
33
34
  "./guardrails": {
34
35
  "import": "./dist/guardrails.js"
35
36
  },
37
+ "./review-agent": {
38
+ "import": "./dist/review-agent.js"
39
+ },
36
40
  "./package.json": "./package.json"
37
41
  },
38
42
  "files": [
39
43
  "dist",
40
44
  "src",
45
+ "supabase",
46
+ ".env.review.example",
41
47
  "CHANGELOG.md",
42
48
  "CREDITS.md",
43
49
  "NOTICE",
44
- "README.md"
50
+ "README.md",
51
+ "REVIEW_AGENT.md"
45
52
  ],
46
53
  "scripts": {
47
54
  "typecheck": "tsc -p tsconfig.json --noEmit",
48
55
  "build": "node build.mjs",
49
56
  "test": "vitest --run",
50
57
  "test:watch": "vitest",
58
+ "test:tui": "vitest run src/console src/conductor/tui-controls.test.ts src/kit/external-editor.test.ts src/launch/login-cancellation.test.ts",
59
+ "test:tui:loop": "node scripts/test-tui-loop.mjs",
60
+ "test:tui:pty": "python3 scripts/test-tui-pty.py",
51
61
  "prepublishOnly": "npm test && npm run build",
52
62
  "start": "node dist/entry.js",
53
- "start:help": "node dist/entry.js --help"
63
+ "start:help": "node dist/entry.js --help",
64
+ "review:server": "node dist/review-server.js"
54
65
  },
55
66
  "dependencies": {
56
67
  "@sinclair/typebox": "^0.34.49",
57
68
  "chalk": "^5.6.2",
58
69
  "highlight.js": "^11.11.1",
59
- "indusagi": "^0.13.10",
70
+ "indusagi": "0.13.12",
60
71
  "ink": "^5.2.1",
61
72
  "jiti": "^2.7.0",
62
73
  "marked": "^18.0.4",
63
74
  "react": "^18.3.1",
64
75
  "strip-ansi": "^7.2.0",
76
+ "string-width": "^7.2.0",
65
77
  "ulid": "^3.0.2",
66
78
  "zod": "^4.4.3"
67
79
  },
@@ -70,6 +82,11 @@
70
82
  "@types/react": "^18.3.12",
71
83
  "esbuild": "^0.28.0",
72
84
  "typescript": "^5.7.2",
73
- "vitest": "^2.1.8"
85
+ "vitest": "^4.1.11"
86
+ },
87
+ "overrides": {
88
+ "fast-uri": "3.1.6",
89
+ "hono": "4.13.5",
90
+ "qs": "6.16.0"
74
91
  }
75
- }
92
+ }
@@ -20,13 +20,12 @@
20
20
  * launch wiring needs. When the concrete multi-account vault arrives it replaces
21
21
  * this adapter at the one call site in {@link "./boot"}.
22
22
  */
23
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
24
- import { dirname } from "node:path";
25
23
  import {
26
24
  ensureFreshOAuthCredentials,
27
25
  getOAuthProvider,
28
26
  } from "indusagi/ai";
29
27
  import type { AuthVault } from "../launch.js";
28
+ import { atomicWritePrivateFileSync, readPrivateFileSync } from "../security/private-state.js";
30
29
 
31
30
  interface OAuthRecord {
32
31
  access: string;
@@ -68,8 +67,9 @@ function normaliseRecord(value: unknown): NormalisedRecord | undefined {
68
67
  }
69
68
 
70
69
  function readAuthFile(filePath: string): Record<string, Record<string, NormalisedRecord>> {
70
+ const text = readPrivateFileSync(filePath);
71
+ if (text === null) return {};
71
72
  try {
72
- const text = readFileSync(filePath, "utf8");
73
73
  const parsed = JSON.parse(text);
74
74
  if (parsed === null || typeof parsed !== "object") return {};
75
75
  const file: Record<string, Record<string, NormalisedRecord>> = {};
@@ -92,8 +92,7 @@ function writeAuthFile(
92
92
  filePath: string,
93
93
  data: Record<string, Record<string, NormalisedRecord>>,
94
94
  ): void {
95
- mkdirSync(dirname(filePath), { recursive: true });
96
- writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, { mode: 384 });
95
+ atomicWritePrivateFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`);
97
96
  }
98
97
 
99
98
  function toCredentials(record: OAuthStoredRecord): OAuthRecord {
@@ -131,7 +131,7 @@ export interface Invocation {
131
131
  readonly cwd?: string;
132
132
  /** Named credential account to authenticate with (`--account`). */
133
133
  readonly account?: string;
134
- /** Reasoning effort (`--thinking`): off / minimal / low / medium / high / xhigh. */
134
+ /** Reasoning effort (`--thinking`): off / minimal / low / medium / high / xhigh / max (model-dependent). */
135
135
  readonly thinking?: string;
136
136
  /** Replacement system prompt (`--system`); a path is read as a file, else literal text. */
137
137
  readonly system?: string;
@@ -35,8 +35,82 @@ import {
35
35
  type InterceptorChain,
36
36
  createAddonHost,
37
37
  } from "../../addons/index.js";
38
+ import { createHash } from "node:crypto";
39
+ import { lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
40
+ import { join, relative, resolve } from "node:path";
41
+ import { atomicWritePrivateFileSync, readPrivateFileSync } from "../../security/private-state.js";
38
42
  import type { BootContext } from "../contract.js";
39
43
 
44
+ interface AddonTrustRecord {
45
+ readonly root: string;
46
+ readonly digest: string;
47
+ readonly approvedAt: string;
48
+ }
49
+
50
+ interface AddonTrustStore {
51
+ readonly version: 1;
52
+ readonly projects: readonly AddonTrustRecord[];
53
+ }
54
+
55
+ const TRUST_FILE = "addon-trust.json";
56
+
57
+ function hashAddonTree(addonsDir: string): string | null {
58
+ let root: string;
59
+ try {
60
+ const rootStat = lstatSync(addonsDir);
61
+ if (rootStat.isSymbolicLink() || !rootStat.isDirectory()) return null;
62
+ root = realpathSync(addonsDir);
63
+ } catch (error) {
64
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
65
+ throw error;
66
+ }
67
+ const files: string[] = [];
68
+ const visit = (dir: string): void => {
69
+ for (const entry of readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
70
+ const path = join(dir, entry.name);
71
+ const info = lstatSync(path);
72
+ if (info.isSymbolicLink()) throw new Error(`Refusing symlink in project addon tree: ${path}`);
73
+ if (info.isDirectory()) visit(path);
74
+ else if (info.isFile()) files.push(path);
75
+ else throw new Error(`Refusing special file in project addon tree: ${path}`);
76
+ }
77
+ };
78
+ visit(root);
79
+ const hash = createHash("sha256");
80
+ for (const file of files) {
81
+ hash.update(relative(root, file));
82
+ hash.update("\0");
83
+ hash.update(readFileSync(file));
84
+ hash.update("\0");
85
+ }
86
+ return hash.digest("hex");
87
+ }
88
+
89
+ function readTrustStore(path: string): AddonTrustStore {
90
+ const raw = readPrivateFileSync(path);
91
+ if (raw === null) return { version: 1, projects: [] };
92
+ try {
93
+ const parsed = JSON.parse(raw) as Partial<AddonTrustStore>;
94
+ if (parsed.version !== 1 || !Array.isArray(parsed.projects)) return { version: 1, projects: [] };
95
+ return {
96
+ version: 1,
97
+ projects: parsed.projects.filter((record): record is AddonTrustRecord =>
98
+ typeof record?.root === "string" &&
99
+ typeof record?.digest === "string" &&
100
+ typeof record?.approvedAt === "string"),
101
+ };
102
+ } catch {
103
+ return { version: 1, projects: [] };
104
+ }
105
+ }
106
+
107
+ function persistTrust(path: string, root: string, digest: string): void {
108
+ const store = readTrustStore(path);
109
+ const projects = store.projects.filter((record) => record.root !== root);
110
+ projects.push({ root, digest, approvedAt: new Date().toISOString() });
111
+ atomicWritePrivateFileSync(path, `${JSON.stringify({ version: 1, projects }, null, 2)}\n`);
112
+ }
113
+
40
114
  /**
41
115
  * Build and populate the addon host for a run, returning its wired
42
116
  * {@link AddonSurfaceBundle}.
@@ -54,10 +128,44 @@ import type { BootContext } from "../contract.js";
54
128
  */
55
129
  export async function buildAddonHost(ctx: BootContext): Promise<AddonSurfaceBundle> {
56
130
  const host = createAddonHost({ handles: {} });
57
- host.onFault(() => {
58
- // swallow addon load faults so a broken addon cannot sink the session
131
+ host.onFault((fault) => {
132
+ process.stderr.write(`Addon ${fault.kind} fault: ${fault.message}\n`);
59
133
  });
60
- return host.loadAll({ workspace: ctx.invocation.cwd ?? process.cwd() });
134
+ const workspace = resolve(ctx.invocation.cwd ?? process.cwd());
135
+ const addonsDir = join(workspace, ".indus", "addons");
136
+ let canonicalRoot: string;
137
+ let digest: string | null;
138
+ try {
139
+ canonicalRoot = realpathSync(workspace);
140
+ digest = hashAddonTree(addonsDir);
141
+ } catch (error) {
142
+ process.stderr.write(`Project addons blocked: ${error instanceof Error ? error.message : String(error)}\n`);
143
+ return host.loadAll({ explicitPaths: [] });
144
+ }
145
+ if (digest === null) return host.loadAll({ explicitPaths: [] });
146
+
147
+ const trustPath = join(ctx.workspace.profileDir, TRUST_FILE);
148
+ const approvedNow = ctx.invocation.flags["trust-project-addons"] === true;
149
+ let trusted = false;
150
+ try {
151
+ const store = readTrustStore(trustPath);
152
+ trusted = store.projects.some((record) => record.root === canonicalRoot && record.digest === digest);
153
+ if (!trusted && approvedNow) {
154
+ persistTrust(trustPath, canonicalRoot, digest);
155
+ trusted = true;
156
+ }
157
+ } catch (error) {
158
+ process.stderr.write(`Project addons blocked: ${error instanceof Error ? error.message : String(error)}\n`);
159
+ return host.loadAll({ explicitPaths: [] });
160
+ }
161
+
162
+ if (!trusted) {
163
+ process.stderr.write(
164
+ `Project addons are disabled until explicitly trusted for this exact content. Re-run with --trust-project-addons to approve ${canonicalRoot}.\n`,
165
+ );
166
+ return host.loadAll({ explicitPaths: [] });
167
+ }
168
+ return host.loadAll({ workspace: canonicalRoot });
61
169
  }
62
170
 
63
171
  /**
@@ -0,0 +1,107 @@
1
+ import { appendFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import type { AgentTool } from "../../addons/index.js";
4
+
5
+ type BenchmarkEnv = Readonly<Record<string, string | undefined>>;
6
+
7
+ const GUARDED_TOOLS = new Set(["read", "write", "edit", "grep", "find", "ls", "bash"]);
8
+ const PATH_KEYS = new Set(["path", "file", "file_path", "directory", "dir", "cwd", "search_path"]);
9
+ const FORBIDDEN_ABSOLUTE = [
10
+ "/tests",
11
+ "/solution",
12
+ "/solutions",
13
+ "/reference",
14
+ "/reference_solution",
15
+ "/reference-solutions",
16
+ "/reward.txt",
17
+ "/rewards",
18
+ "/grader",
19
+ "/verifier",
20
+ ];
21
+ const FORBIDDEN_BASENAMES = new Set([
22
+ "solution.sh",
23
+ "solve.sh",
24
+ "reward.txt",
25
+ "expected_answer.txt",
26
+ "expected_output.txt",
27
+ ]);
28
+
29
+ function normalizeCandidate(value: string, cwd: string): string {
30
+ return path.resolve(cwd, value);
31
+ }
32
+
33
+ function forbiddenPath(value: string, cwd: string): boolean {
34
+ const resolved = normalizeCandidate(value, cwd);
35
+ const base = path.basename(resolved).toLowerCase();
36
+ if (FORBIDDEN_BASENAMES.has(base)) return true;
37
+ return FORBIDDEN_ABSOLUTE.some((prefix) => resolved === prefix || resolved.startsWith(`${prefix}/`));
38
+ }
39
+
40
+ function pathValues(args: unknown): string[] {
41
+ if (args === null || typeof args !== "object" || Array.isArray(args)) return [];
42
+ const values: string[] = [];
43
+ for (const [key, value] of Object.entries(args as Record<string, unknown>)) {
44
+ if (PATH_KEYS.has(key) && typeof value === "string") values.push(value);
45
+ }
46
+ return values;
47
+ }
48
+
49
+ function forbiddenShell(command: string, cwd: string): boolean {
50
+ const lower = command.toLowerCase();
51
+ if ([...FORBIDDEN_BASENAMES].some((name) => lower.includes(name))) return true;
52
+ if (FORBIDDEN_ABSOLUTE.some((prefix) => new RegExp(`(^|[\\s'\"=])${prefix.replaceAll("/", "\\/")}(?:[/\\s'\";|&]|$)`, "i").test(command))) return true;
53
+ if (/(^|[\s'"=])\.\.\/(?:tests|solutions?|reference(?:[-_]solutions?)?|grader|verifier)(?:[/\s'";|&]|$)/i.test(command)) return true;
54
+ if (/\b(?:find|ls|grep|rg)\s+(?:-[^\s]+\s+)*\/(?:\s|$)/i.test(command)) return true;
55
+ const retrievesOnline = /\b(?:curl|wget|git\s+clone|gh\s+repo\s+clone)\b/i.test(command);
56
+ const benchmarkMaterial = /terminal[-_ ]bench|reference[-_ ]solution|solution\.sh|solve\.sh|benchmark[-_ ]solution/i.test(command);
57
+ if (retrievesOnline && benchmarkMaterial) return true;
58
+ return false;
59
+ }
60
+
61
+ function denialReason(tool: string, args: unknown, cwd: string): string | undefined {
62
+ if (tool === "bash") {
63
+ const command = typeof (args as { command?: unknown } | null)?.command === "string"
64
+ ? (args as { command: string }).command
65
+ : "";
66
+ if (forbiddenShell(command, cwd)) return "Terminal-Bench integrity guard blocked benchmark-owned or solution material.";
67
+ return undefined;
68
+ }
69
+ if (pathValues(args).some((candidate) => forbiddenPath(candidate, cwd))) {
70
+ return "Terminal-Bench integrity guard blocked benchmark-owned or solution material.";
71
+ }
72
+ return undefined;
73
+ }
74
+
75
+ function recordDenial(tool: string, callId: string, reason: string, env: BenchmarkEnv): void {
76
+ const auditPath = env.INDUSAGI_BENCHMARK_AUDIT_LOG ?? "/logs/agent/integrity-denials.jsonl";
77
+ try {
78
+ appendFileSync(auditPath, `${JSON.stringify({ type: "integrity_denial", tool, call_id: callId, reason })}\n`, { encoding: "utf8" });
79
+ } catch {
80
+ // Blocking must remain fail-closed even if the optional audit sink is unavailable.
81
+ }
82
+ }
83
+
84
+ function wrapOne(tool: AgentTool, cwd: string, env: BenchmarkEnv): AgentTool {
85
+ const execute = tool.execute.bind(tool);
86
+ return {
87
+ ...tool,
88
+ async execute(callId: string, args: unknown, signal?: AbortSignal, onUpdate?: unknown) {
89
+ const reason = denialReason(tool.name, args, cwd);
90
+ if (reason !== undefined) {
91
+ recordDenial(tool.name, callId, reason, env);
92
+ return {
93
+ content: [{ type: "text" as const, text: reason }],
94
+ details: undefined,
95
+ isError: true,
96
+ };
97
+ }
98
+ return execute(callId, args as never, signal as never, onUpdate as never);
99
+ },
100
+ };
101
+ }
102
+
103
+ /** Apply fail-closed Terminal-Bench integrity checks at every enabled tool boundary. */
104
+ export function wrapToolsForTerminalBench(tools: AgentTool[], cwd: string, env: BenchmarkEnv = process.env): AgentTool[] {
105
+ if (env.INDUSAGI_BENCHMARK_PROFILE !== "terminal-bench") return tools;
106
+ return tools.map((tool) => GUARDED_TOOLS.has(tool.name) ? wrapOne(tool, cwd, env) : tool);
107
+ }
@@ -14,6 +14,7 @@ import { SessionLibrary } from "../../sessions/library.js";
14
14
  import { PreferenceStore } from "../../settings/manager.js";
15
15
  import { sessionScopeDir } from "./session.js";
16
16
  import { createAuthVault } from "../auth-vault.js";
17
+ import type { SessionConductor } from "../../conductor/contract.js";
17
18
  import type { BootContext, Runner } from "../contract.js";
18
19
  import { buildSessionConductor } from "./session.js";
19
20
  import { resolveServerGatewayUrls } from "./server-mode.js";
@@ -28,7 +29,7 @@ const EXIT_OK = 0;
28
29
  * wired here from the boot-layer pieces so the conductor and the UI share one
29
30
  * credential store and one gateway-base-url mapping.
30
31
  */
31
- export function buildOverlayServices(ctx: BootContext, conductor: { updateGatewayBaseUrls(map: Record<string, string>): void }) {
32
+ export function buildOverlayServices(ctx: BootContext, conductor: SessionConductor) {
32
33
  const cwd = ctx.invocation.cwd ?? process.cwd();
33
34
  const vault = createAuthVault(ctx.workspace.authPath);
34
35
  return {
@@ -9,7 +9,7 @@
9
9
  * is constructed until the first turn runs.
10
10
  */
11
11
  import { existsSync, readFileSync } from "node:fs";
12
- import { join } from "node:path";
12
+ import { isAbsolute, join, resolve } from "node:path";
13
13
  import { homedir } from "node:os";
14
14
  import { createMCPAgentToolFactory, loadMCPConfig, MCPClientPool } from "indusagi/mcp";
15
15
  import {
@@ -58,8 +58,12 @@ import { createAuthVault } from "../auth-vault.js";
58
58
  import { TOKEN_FILE, hasValidServerToken, getServerToken } from "../server-token.js";
59
59
  import { resolveServerMode, resolveServerGatewayUrls, serverTierProviders } from "./server-mode.js";
60
60
  import { DiskMemoryStore, memoryDirFor, loadMemoryDoc } from "./memdir.js";
61
+ import { isReasoningEffortSupported } from "../../models.js";
62
+ import type { CodingAgentThinkingLevel } from "../../conductor/contract.js";
61
63
  import { buildAddonHost, wrapToolsWithAddons, concatAddonTools } from "./addon-wiring.js";
64
+ import { wrapToolsForTerminalBench } from "./benchmark-integrity.js";
62
65
  import type { BootContext, Invocation } from "../contract.js";
66
+ import { readPrivateFileSync } from "../../security/private-state.js";
63
67
 
64
68
  interface AuthFileData {
65
69
  [provider: string]: unknown;
@@ -68,8 +72,9 @@ interface AuthFileData {
68
72
  function authenticatedProviders(ctx: BootContext): string[] {
69
73
  try {
70
74
  const path = ctx.workspace.authPath;
71
- if (!existsSync(path)) return [];
72
- const data = JSON.parse(readFileSync(path, "utf8")) as AuthFileData;
75
+ const raw = readPrivateFileSync(path);
76
+ if (raw === null) return [];
77
+ const data = JSON.parse(raw) as AuthFileData;
73
78
  return Object.keys(data).filter((id) => {
74
79
  const accounts = data[id];
75
80
  return id.length > 0 && accounts !== null && typeof accounts === "object" && Object.keys(accounts as object).length > 0;
@@ -219,10 +224,12 @@ function buildKeyResolver(
219
224
  };
220
225
  }
221
226
 
222
- const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh"]);
227
+ const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
223
228
 
224
- function resolveThinkingLevel(raw: string | undefined): string | undefined {
225
- return raw !== undefined && THINKING_LEVELS.has(raw) ? raw : undefined;
229
+ export function resolveThinkingLevel(raw: string | undefined, modelId?: string): CodingAgentThinkingLevel | undefined {
230
+ return raw !== undefined && THINKING_LEVELS.has(raw) && isReasoningEffortSupported(modelId, raw)
231
+ ? raw as CodingAgentThinkingLevel
232
+ : undefined;
226
233
  }
227
234
 
228
235
  function resolveText(value: string): string {
@@ -299,8 +306,11 @@ export function composeSystem(
299
306
  skills: readonly SkillCard[] = [],
300
307
  memoryDoc?: ContextDoc,
301
308
  ): string {
309
+ const approvedExternalPaths = Array.isArray(inv.flags["allow-context-import"])
310
+ ? inv.flags["allow-context-import"].filter((path): path is string => typeof path === "string").map((path) => isAbsolute(path) ? path : resolve(cwd, path))
311
+ : [];
302
312
  const contextDocs = [
303
- ...gatherContextDocs(cwd, homedir()),
313
+ ...gatherContextDocs(cwd, homedir(), { approvedExternalPaths }),
304
314
  ...memoryDoc ? [memoryDoc] : [],
305
315
  ];
306
316
  let base = inv.system !== undefined ? resolveText(inv.system) : composeBriefing({ tools, contextDocs, skills });
@@ -427,13 +437,14 @@ function lastUserTurnStart(messages: Array<{ role: string }>): number {
427
437
 
428
438
  const condenseTranscript = async (
429
439
  messages: Array<{ role: string }>,
430
- opts?: { model?: unknown; apiKey?: string; signal?: AbortSignal; force?: boolean },
440
+ opts?: { model?: unknown; apiKey?: string; signal?: AbortSignal; force?: boolean; instructions?: string },
431
441
  ): Promise<Array<{ role: string }>> => {
432
442
  const model = opts?.model;
433
443
  const deps = model ? {
434
444
  model,
435
445
  ...opts?.apiKey !== undefined ? { apiKey: opts.apiKey } : {},
436
446
  ...opts?.signal !== undefined ? { signal: opts.signal } : {},
447
+ ...opts?.instructions !== undefined ? { instructions: opts.instructions } : {},
437
448
  } : {};
438
449
  const cleared = clearStaleToolResults(messages as never) as Array<{ role: string }>;
439
450
  if (opts?.force) {
@@ -471,12 +482,12 @@ function gatherPermissionRules(store: PreferenceStore): PermissionRule[] {
471
482
  return rules;
472
483
  }
473
484
 
474
- function resolveDefaultMode(store: PreferenceStore): PermissionMode {
485
+ export function resolveDefaultMode(store: PreferenceStore): PermissionMode {
475
486
  for (const tier of [store.projectOverrides(), store.globalDefaults()]) {
476
487
  const mode = tier.permissions?.defaultMode;
477
488
  if (mode !== undefined && isPermissionMode(mode)) return mode;
478
489
  }
479
- return "default";
490
+ return "acceptEdits";
480
491
  }
481
492
 
482
493
  function readOnlyNamesFromDeck(tools: AgentTool[]): Set<string> {
@@ -594,10 +605,13 @@ export async function buildSessionConductor(ctx: BootContext) {
594
605
  basePath: typeof inv.sarvamBasePath === "string" ? inv.sarvamBasePath : typeof inv.flags?.["sarvam-base-path"] === "string" ? inv.flags["sarvam-base-path"] : undefined,
595
606
  })),
596
607
  ];
597
- const tools = wrapToolsWithAddons(concatAddonTools(deck, addons), addons);
608
+ const tools = wrapToolsForTerminalBench(
609
+ wrapToolsWithAddons(concatAddonTools(deck, addons), addons),
610
+ cwd,
611
+ );
598
612
  const skills = gatherModelSkills(cwd);
599
613
  const system = composeSystem(tools, inv, cwd, skills, memoryDoc);
600
- const thinking = resolveThinkingLevel(inv.thinking);
614
+ const thinking = resolveThinkingLevel(inv.thinking, modelId);
601
615
  const fallbackModelId = inv.fallbackModelId;
602
616
  const planReachable = inv.mode === "repl";
603
617
  const canUseTool = buildPermissionGate(permissionRules, permissionMode, tools, planReachable);
@@ -18,9 +18,10 @@
18
18
  * ISO timestamp, on disk) conversion, which lets {@link isServerTokenExpired}
19
19
  * decide freshness without re-deriving the clock offset.
20
20
  */
21
- import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
21
+ import { unlink } from "node:fs/promises";
22
22
  import os from "node:os";
23
23
  import path from "node:path";
24
+ import { atomicWritePrivateFileSync, readPrivateFileSync } from "../security/private-state.js";
24
25
 
25
26
  /** Root of the indus-server. Used to build the gateway base URL and the device-login endpoints. Any trailing slash(es) are stripped so callers can append paths with a single separator. */
26
27
  export const INDUS_SERVER_URL: string = process.env.INDUS_SERVER_URL?.replace(/\/+$/, "") ?? "https://indus-server.vercel.app";
@@ -29,6 +30,16 @@ export const CONFIG_DIR: string = path.join(os.homedir(), ".indusagi", "agent");
29
30
  /** Absolute path of the JSON file the session token is persisted to. */
30
31
  export const TOKEN_FILE: string = path.join(CONFIG_DIR, "server-token.json");
31
32
 
33
+ export interface ServerTokenPaths {
34
+ readonly configDir?: string;
35
+ readonly tokenFile?: string;
36
+ }
37
+
38
+ function resolveTokenPaths(paths: ServerTokenPaths = {}): { configDir: string; tokenFile: string } {
39
+ const configDir = paths.configDir ?? CONFIG_DIR;
40
+ return { configDir, tokenFile: paths.tokenFile ?? path.join(configDir, "server-token.json") };
41
+ }
42
+
32
43
  /** On-disk token shape (what {@link storeServerToken} writes / {@link getServerToken} returns). */
33
44
  export interface TokenData {
34
45
  /** The better-auth session token, used as the gateway api key in server mode. */
@@ -65,9 +76,10 @@ export interface ServerTokenInput {
65
76
  *
66
77
  * @returns the parsed {@link TokenData}, or `null` when nothing is stored.
67
78
  */
68
- export async function getServerToken(): Promise<TokenData | null> {
79
+ export async function getServerToken(paths: ServerTokenPaths = {}): Promise<TokenData | null> {
69
80
  try {
70
- const data = await readFile(TOKEN_FILE, "utf8");
81
+ const data = readPrivateFileSync(resolveTokenPaths(paths).tokenFile);
82
+ if (data === null) return null;
71
83
  return JSON.parse(data);
72
84
  } catch {
73
85
  return null;
@@ -83,9 +95,9 @@ export async function getServerToken(): Promise<TokenData | null> {
83
95
  * @param token the device-flow token payload.
84
96
  * @returns `true` on success, `false` if the write failed.
85
97
  */
86
- export async function storeServerToken(token: ServerTokenInput): Promise<boolean> {
98
+ export async function storeServerToken(token: ServerTokenInput, paths: ServerTokenPaths = {}): Promise<boolean> {
87
99
  try {
88
- await mkdir(CONFIG_DIR, { recursive: true });
100
+ const { tokenFile } = resolveTokenPaths(paths);
89
101
  const tokenData = {
90
102
  access_token: token.access_token,
91
103
  refresh_token: token.refresh_token,
@@ -94,10 +106,7 @@ export async function storeServerToken(token: ServerTokenInput): Promise<boolean
94
106
  expires_at: token.expires_in !== undefined ? new Date(Date.now() + token.expires_in * 1000).toISOString() : null,
95
107
  created_at: new Date().toISOString(),
96
108
  };
97
- await writeFile(TOKEN_FILE, `${JSON.stringify(tokenData, null, 2)}\n`, {
98
- encoding: "utf8",
99
- mode: 384,
100
- });
109
+ atomicWritePrivateFileSync(tokenFile, `${JSON.stringify(tokenData, null, 2)}\n`);
101
110
  return true;
102
111
  } catch (error) {
103
112
  console.error("Failed to store server token:", (error as Error).message);
@@ -111,9 +120,9 @@ export async function storeServerToken(token: ServerTokenInput): Promise<boolean
111
120
  *
112
121
  * @returns `true` when a file was deleted, `false` when none existed / removal failed.
113
122
  */
114
- export async function clearServerToken(): Promise<boolean> {
123
+ export async function clearServerToken(paths: ServerTokenPaths = {}): Promise<boolean> {
115
124
  try {
116
- await unlink(TOKEN_FILE);
125
+ await unlink(resolveTokenPaths(paths).tokenFile);
117
126
  return true;
118
127
  } catch {
119
128
  return false;
@@ -29,6 +29,7 @@ import { tokenizeInvocation } from "./invocation.js";
29
29
  import { applyUpgrades } from "./upgrade/apply.js";
30
30
  import { ensureDirs } from "../workspace/locator.js";
31
31
  import type { BootContext, Stage } from "./contract.js";
32
+ import { createCodingAgentModelRegistry } from "../models.js";
32
33
 
33
34
  /**
34
35
  * Materialise the resolved workspace directories on disk.
@@ -110,11 +111,7 @@ async function loadDefaultSettings(): Promise<unknown> {
110
111
  }
111
112
 
112
113
  async function loadModelRegistry(): Promise<unknown> {
113
- const mod = (await import("indusagi/ai")) as { ModelRegistry?: unknown; modelRegistry?: unknown };
114
- if (typeof mod.ModelRegistry === "function") {
115
- return new (mod.ModelRegistry as new () => unknown)();
116
- }
117
- return mod.modelRegistry;
114
+ return createCodingAgentModelRegistry();
118
115
  }
119
116
 
120
117
  /**
@@ -164,4 +161,4 @@ export async function runStages(initial: BootContext, stages: readonly Stage[] =
164
161
  return ctx;
165
162
  }
166
163
 
167
- export { locateWorkspace, upgrade, buildInvocation, resolveResources, selectRunnerStage };
164
+ export { locateWorkspace, upgrade, buildInvocation, resolveResources, selectRunnerStage, loadModelRegistry };