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.
- package/.env.review.example +25 -0
- package/README.md +30 -0
- package/REVIEW_AGENT.md +137 -0
- package/dist/entry.js +1861 -979
- package/dist/guardrails.js +212 -36
- package/dist/index.js +2829 -1057
- package/dist/review-agent.js +4461 -0
- package/dist/review-server.js +4633 -0
- package/package.json +24 -7
- package/src/boot/auth-vault.ts +4 -5
- package/src/boot/contract.ts +1 -1
- package/src/boot/runners/addon-wiring.ts +111 -3
- package/src/boot/runners/benchmark-integrity.ts +107 -0
- package/src/boot/runners/repl-runner.ts +2 -1
- package/src/boot/runners/session.ts +26 -12
- package/src/boot/server-token.ts +20 -11
- package/src/boot/stages.ts +3 -6
- package/src/briefing/context-docs.ts +144 -12
- package/src/channels/oneshot.ts +6 -0
- package/src/conductor/catalog/catalog.ts +3 -1
- package/src/conductor/conductor.ts +77 -17
- package/src/conductor/contract.ts +9 -7
- package/src/conductor/index.ts +1 -1
- package/src/conductor/transcript-store/store.ts +4 -8
- package/src/console/auth-status.ts +9 -4
- package/src/console/components/AgentsView.ts +2 -2
- package/src/console/components/BackgroundAgents.ts +15 -2
- package/src/console/components/Banner.ts +20 -9
- package/src/console/components/Composer.ts +57 -82
- package/src/console/components/ScrollViewport.ts +46 -0
- package/src/console/components/TerminalConsole.ts +165 -162
- package/src/console/components/WorkingIndicator.ts +6 -5
- package/src/console/components/banner-sweep.ts +5 -1
- package/src/console/components/welcome.ts +3 -3
- package/src/console/contract.ts +10 -5
- package/src/console/index.ts +7 -7
- package/src/console/input/complete.ts +2 -2
- package/src/console/input/dir-reader.ts +3 -1
- package/src/console/input/graphemes.ts +22 -0
- package/src/console/input/keymap.ts +6 -4
- package/src/console/mount.ts +45 -3
- package/src/console/overlays/auth.ts +28 -8
- package/src/console/overlays/boards.ts +13 -40
- package/src/console/overlays/host.ts +10 -0
- package/src/console/overlays/pickers.ts +35 -30
- package/src/console/reducer.ts +12 -5
- package/src/console/slash/builtins.ts +4 -3
- package/src/console/slash/commands/integrations.ts +13 -14
- package/src/console/slash/commands/transcript.ts +3 -8
- package/src/console/slash/commands/workbench.ts +4 -4
- package/src/console/test-support.ts +48 -0
- package/src/index.ts +1 -0
- package/src/integrations/sarvam/config.ts +15 -13
- package/src/kit/external-editor.ts +17 -1
- package/src/kit/run-shell.ts +31 -0
- package/src/launch/catalog.ts +4 -2
- package/src/launch/contract.ts +1 -0
- package/src/launch/invocation/flags.ts +13 -1
- package/src/launch/invocation/read.ts +4 -3
- package/src/launch/login.ts +23 -10
- package/src/launch/oauth.ts +3 -0
- package/src/models.ts +166 -0
- package/src/review-agent/config.ts +75 -0
- package/src/review-agent/contract.ts +59 -0
- package/src/review-agent/github.ts +242 -0
- package/src/review-agent/index.ts +8 -0
- package/src/review-agent/prompt.ts +132 -0
- package/src/review-agent/reviewer.ts +118 -0
- package/src/review-agent/server.ts +221 -0
- package/src/review-agent/service.ts +165 -0
- package/src/review-agent/supabase.ts +209 -0
- package/src/review-agent/webhook.ts +76 -0
- package/src/security/private-state.ts +122 -0
- package/src/window-budget/summarize/condense.ts +4 -1
- package/src/workspace/brand.ts +1 -1
- package/src/workspace/locator.ts +2 -2
- package/supabase/migrations/20260827031803_indusagi_review_agent.sql +109 -0
- package/supabase/migrations/20260827035747_atomic_webhook_enqueue.sql +75 -0
- package/supabase/migrations/20260827040104_check_run_reconciliation.sql +8 -0
- package/supabase/migrations/20260827041049_owned_job_leases.sql +80 -0
- 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.
|
|
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": "
|
|
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": "^
|
|
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
|
+
}
|
package/src/boot/auth-vault.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
package/src/boot/contract.ts
CHANGED
|
@@ -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
|
-
|
|
131
|
+
host.onFault((fault) => {
|
|
132
|
+
process.stderr.write(`Addon ${fault.kind} fault: ${fault.message}\n`);
|
|
59
133
|
});
|
|
60
|
-
|
|
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:
|
|
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
|
-
|
|
72
|
-
|
|
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):
|
|
225
|
-
return raw !== undefined && THINKING_LEVELS.has(raw)
|
|
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 "
|
|
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 =
|
|
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);
|
package/src/boot/server-token.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
125
|
+
await unlink(resolveTokenPaths(paths).tokenFile);
|
|
117
126
|
return true;
|
|
118
127
|
} catch {
|
|
119
128
|
return false;
|
package/src/boot/stages.ts
CHANGED
|
@@ -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
|
-
|
|
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 };
|