@cruxy/cli 0.29.0 → 0.29.3
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/dist/agent/budget.d.ts +52 -0
- package/dist/{subagent → agent}/budget.js +2 -8
- package/dist/agent/loop.d.ts +15 -0
- package/dist/agent/loop.js +11 -2
- package/dist/agent/session.d.ts +32 -7
- package/dist/agent/session.js +89 -28
- package/dist/cli/commands/mcp.js +1 -1
- package/dist/config/credentials.d.ts +3 -4
- package/dist/config/credentials.js +74 -21
- package/dist/config/owner-only.d.ts +19 -0
- package/dist/config/owner-only.js +114 -0
- package/dist/config/schema.d.ts +86 -32
- package/dist/config/schema.js +21 -0
- package/dist/errors/constructors.d.ts +9 -0
- package/dist/errors/constructors.js +27 -0
- package/dist/errors/types.d.ts +6 -0
- package/dist/errors/types.js +7 -0
- package/dist/indexing/service.js +4 -1
- package/dist/jobs/manager.js +2 -1
- package/dist/memory/types.d.ts +2 -2
- package/dist/subagent/index.d.ts +0 -1
- package/dist/subagent/index.js +0 -1
- package/dist/subagent/orchestrator.js +1 -1
- package/dist/subagent/types.d.ts +2 -13
- package/dist/testing/runner.js +5 -17
- package/dist/tools/shell/exec.js +5 -19
- package/dist/utils/child-tree.d.ts +9 -11
- package/dist/utils/child-tree.js +11 -25
- package/dist/utils/process-tree.d.ts +16 -0
- package/dist/utils/process-tree.js +81 -0
- package/package.json +5 -3
- package/dist/subagent/budget.d.ts +0 -34
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { chmodSync, statSync } from "node:fs";
|
|
3
|
+
/**
|
|
4
|
+
* Cross-platform "owner-only" file/dir permissions — the one place that knows
|
|
5
|
+
* how to make a path readable by nobody but its owner, and how to check whether
|
|
6
|
+
* it already is.
|
|
7
|
+
*
|
|
8
|
+
* POSIX is `chmod` (`0600` file / `0700` dir). Windows has no POSIX modes —
|
|
9
|
+
* `fs.chmod` there only toggles the read-only attribute and never touches the
|
|
10
|
+
* NTFS ACL (a libuv limitation), so it is worthless for confidentiality. The
|
|
11
|
+
* Windows equivalent is an ACL edit via `icacls`: strip the inherited ACEs (the
|
|
12
|
+
* only source of "others" access on a fresh file) and grant Full to exactly the
|
|
13
|
+
* current user's SID.
|
|
14
|
+
*
|
|
15
|
+
* Every function here THROWS rather than swallowing a failure — the caller (the
|
|
16
|
+
* credential store) turns that into a loud refusal to persist a secret it can't
|
|
17
|
+
* secure. Nothing here silently claims success.
|
|
18
|
+
*/
|
|
19
|
+
const isWindows = process.platform === "win32";
|
|
20
|
+
/** `undefined` = not yet resolved, `null` = resolution failed this process. */
|
|
21
|
+
let cachedSid;
|
|
22
|
+
/**
|
|
23
|
+
* The current user's SID (e.g. `S-1-5-21-…`) via `whoami /user`, resolved once
|
|
24
|
+
* and cached. We grant to the SID — never a name — so the ACL is correct
|
|
25
|
+
* regardless of the machine's display language (`BUILTIN\Users` etc. localise).
|
|
26
|
+
* Returns `null` if it cannot be determined.
|
|
27
|
+
*/
|
|
28
|
+
function currentUserSid() {
|
|
29
|
+
if (cachedSid !== undefined)
|
|
30
|
+
return cachedSid;
|
|
31
|
+
try {
|
|
32
|
+
const out = execFileSync("whoami", ["/user", "/fo", "csv", "/nh"], {
|
|
33
|
+
encoding: "utf8",
|
|
34
|
+
});
|
|
35
|
+
const m = out.match(/S-1-[0-9-]+/);
|
|
36
|
+
cachedSid = m ? m[0] : null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
cachedSid = null;
|
|
40
|
+
}
|
|
41
|
+
return cachedSid;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Make `path` owner-only, or throw. On Windows the ACL is verified after the
|
|
45
|
+
* edit (both the `icacls` exit code AND a read-back), so a returned call is a
|
|
46
|
+
* genuine guarantee, never a best-effort attempt.
|
|
47
|
+
*
|
|
48
|
+
* @throws if owner-only permissions cannot be established (non-zero `icacls`,
|
|
49
|
+
* an unresolvable SID, a filesystem without ACLs/modes, …).
|
|
50
|
+
*/
|
|
51
|
+
export function enforceOwnerOnly(path, opts = {}) {
|
|
52
|
+
const directory = opts.directory ?? false;
|
|
53
|
+
if (isWindows) {
|
|
54
|
+
const sid = currentUserSid();
|
|
55
|
+
if (!sid) {
|
|
56
|
+
throw new Error("could not resolve the current user's SID (whoami failed) — cannot set an owner-only ACL");
|
|
57
|
+
}
|
|
58
|
+
// /inheritance:r removes inherited ACEs (where any "others" access comes
|
|
59
|
+
// from); /grant:r replaces the DACL with exactly this one grant. A directory
|
|
60
|
+
// grant carries (OI)(CI) so files created inside inherit owner-only at birth.
|
|
61
|
+
// The SID must be written `*S-1-…` — a bare token is read as an account NAME
|
|
62
|
+
// ("No mapping between account names and security IDs" otherwise).
|
|
63
|
+
const principal = `*${sid}`;
|
|
64
|
+
const grant = directory ? `${principal}:(OI)(CI)F` : `${principal}:F`;
|
|
65
|
+
try {
|
|
66
|
+
execFileSync("icacls", [path, "/inheritance:r", "/grant:r", grant], {
|
|
67
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const stderr = err.stderr?.toString().trim();
|
|
72
|
+
throw new Error(`icacls could not restrict "${path}"${stderr ? `: ${stderr}` : ""}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
chmodSync(path, directory ? 0o700 : 0o600);
|
|
77
|
+
}
|
|
78
|
+
// Verify the end state rather than trust the set — the guarantee is the point.
|
|
79
|
+
if (!isOwnerOnly(path)) {
|
|
80
|
+
throw new Error(`"${path}" is not owner-only after attempting to restrict it`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Whether `path` is currently owner-only. POSIX: no group/other bits. Windows:
|
|
85
|
+
* best-effort — the ACL names no broad principal (Everyone / Authenticated
|
|
86
|
+
* Users / Users). Used both to verify {@link enforceOwnerOnly} and to warn on a
|
|
87
|
+
* pre-existing store with loose permissions. Returns `true` when it genuinely
|
|
88
|
+
* cannot tell (a missing tool), so a warning path never cries wolf.
|
|
89
|
+
*/
|
|
90
|
+
export function isOwnerOnly(path) {
|
|
91
|
+
if (!isWindows) {
|
|
92
|
+
return (statSync(path).mode & 0o077) === 0;
|
|
93
|
+
}
|
|
94
|
+
let out;
|
|
95
|
+
try {
|
|
96
|
+
out = execFileSync("icacls", [path], { encoding: "utf8" });
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return true; // can't inspect → don't raise a false alarm
|
|
100
|
+
}
|
|
101
|
+
// `icacls <path>` resolves SIDs to names (locale-dependent on non-English
|
|
102
|
+
// Windows, hence best-effort): flag the well-known broad principals by name
|
|
103
|
+
// and by SID for the cases icacls leaves a SID unresolved.
|
|
104
|
+
const broad = [
|
|
105
|
+
/\bEveryone\b/i,
|
|
106
|
+
/\bAuthenticated Users\b/i,
|
|
107
|
+
/\bBUILTIN\\Users\b/i,
|
|
108
|
+
/\\Users:/i,
|
|
109
|
+
/S-1-1-0/, // Everyone
|
|
110
|
+
/S-1-5-11/, // Authenticated Users
|
|
111
|
+
/S-1-5-32-545/, // BUILTIN\Users
|
|
112
|
+
];
|
|
113
|
+
return !broad.some((re) => re.test(out));
|
|
114
|
+
}
|
package/dist/config/schema.d.ts
CHANGED
|
@@ -41,6 +41,19 @@ export declare const AgentConfigSchema: z.ZodObject<{
|
|
|
41
41
|
* `maxIterations` — a user may deliberately set it lower or higher.
|
|
42
42
|
*/
|
|
43
43
|
maxIterationsOneShot: z.ZodDefault<z.ZodNumber>;
|
|
44
|
+
/**
|
|
45
|
+
* Optional hard cap on combined input+output tokens for a SINGLE user turn
|
|
46
|
+
* — a last-resort runaway guard, not cost control (cumulative session cost
|
|
47
|
+
* is C.22's concern). OFF by default (0 = off): today's behavior is
|
|
48
|
+
* preserved exactly and no turn is ever token-stopped. When set, a turn that
|
|
49
|
+
* crosses the cap stops at the next turn boundary with a coherent partial
|
|
50
|
+
* history (`stop: "budget"`); in one-shot that fails loud (exit 20), while
|
|
51
|
+
* interactively the human simply continues. Compaction is the everyday
|
|
52
|
+
* pressure valve for context — this only bounds a genuinely runaway turn, so
|
|
53
|
+
* a hostile hard stop mid-work stays opt-in. Reset each `send`, never
|
|
54
|
+
* cumulative across turns.
|
|
55
|
+
*/
|
|
56
|
+
maxTokensPerTurn: z.ZodDefault<z.ZodNumber>;
|
|
44
57
|
/** Skip per-action confirmation prompts. */
|
|
45
58
|
autoApprove: z.ZodDefault<z.ZodBoolean>;
|
|
46
59
|
/** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
|
|
@@ -48,11 +61,13 @@ export declare const AgentConfigSchema: z.ZodObject<{
|
|
|
48
61
|
}, "strict", z.ZodTypeAny, {
|
|
49
62
|
maxIterations: number;
|
|
50
63
|
maxIterationsOneShot: number;
|
|
64
|
+
maxTokensPerTurn: number;
|
|
51
65
|
autoApprove: boolean;
|
|
52
66
|
planMode: boolean;
|
|
53
67
|
}, {
|
|
54
68
|
maxIterations?: number | undefined;
|
|
55
69
|
maxIterationsOneShot?: number | undefined;
|
|
70
|
+
maxTokensPerTurn?: number | undefined;
|
|
56
71
|
autoApprove?: boolean | undefined;
|
|
57
72
|
planMode?: boolean | undefined;
|
|
58
73
|
}>;
|
|
@@ -103,16 +118,26 @@ export declare const ContextConfigSchema: z.ZodObject<{
|
|
|
103
118
|
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
104
119
|
/** Compact once the history estimate exceeds this fraction of maxTokens. */
|
|
105
120
|
compactThreshold: z.ZodDefault<z.ZodNumber>;
|
|
121
|
+
/**
|
|
122
|
+
* Fixed token allowance for request payload that `estimateTokens` never
|
|
123
|
+
* sees — the system prompt and every tool's JSON schema — added to the
|
|
124
|
+
* measured history before the threshold test so the trigger reflects the
|
|
125
|
+
* real request size, not just the visible messages. Roughly the size of
|
|
126
|
+
* the built system prompt plus the default tool catalogue today.
|
|
127
|
+
*/
|
|
128
|
+
reserveTokens: z.ZodDefault<z.ZodNumber>;
|
|
106
129
|
/** Most-recent messages always kept verbatim (a floor; the cut rounds up to
|
|
107
130
|
* a clean turn boundary). */
|
|
108
131
|
keepRecentMessages: z.ZodDefault<z.ZodNumber>;
|
|
109
132
|
}, "strict", z.ZodTypeAny, {
|
|
110
133
|
maxTokens: number;
|
|
111
134
|
compactThreshold: number;
|
|
135
|
+
reserveTokens: number;
|
|
112
136
|
keepRecentMessages: number;
|
|
113
137
|
}, {
|
|
114
138
|
maxTokens?: number | undefined;
|
|
115
139
|
compactThreshold?: number | undefined;
|
|
140
|
+
reserveTokens?: number | undefined;
|
|
116
141
|
keepRecentMessages?: number | undefined;
|
|
117
142
|
}>;
|
|
118
143
|
/**
|
|
@@ -1009,6 +1034,19 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1009
1034
|
* `maxIterations` — a user may deliberately set it lower or higher.
|
|
1010
1035
|
*/
|
|
1011
1036
|
maxIterationsOneShot: z.ZodDefault<z.ZodNumber>;
|
|
1037
|
+
/**
|
|
1038
|
+
* Optional hard cap on combined input+output tokens for a SINGLE user turn
|
|
1039
|
+
* — a last-resort runaway guard, not cost control (cumulative session cost
|
|
1040
|
+
* is C.22's concern). OFF by default (0 = off): today's behavior is
|
|
1041
|
+
* preserved exactly and no turn is ever token-stopped. When set, a turn that
|
|
1042
|
+
* crosses the cap stops at the next turn boundary with a coherent partial
|
|
1043
|
+
* history (`stop: "budget"`); in one-shot that fails loud (exit 20), while
|
|
1044
|
+
* interactively the human simply continues. Compaction is the everyday
|
|
1045
|
+
* pressure valve for context — this only bounds a genuinely runaway turn, so
|
|
1046
|
+
* a hostile hard stop mid-work stays opt-in. Reset each `send`, never
|
|
1047
|
+
* cumulative across turns.
|
|
1048
|
+
*/
|
|
1049
|
+
maxTokensPerTurn: z.ZodDefault<z.ZodNumber>;
|
|
1012
1050
|
/** Skip per-action confirmation prompts. */
|
|
1013
1051
|
autoApprove: z.ZodDefault<z.ZodBoolean>;
|
|
1014
1052
|
/** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
|
|
@@ -1016,11 +1054,13 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1016
1054
|
}, "strict", z.ZodTypeAny, {
|
|
1017
1055
|
maxIterations: number;
|
|
1018
1056
|
maxIterationsOneShot: number;
|
|
1057
|
+
maxTokensPerTurn: number;
|
|
1019
1058
|
autoApprove: boolean;
|
|
1020
1059
|
planMode: boolean;
|
|
1021
1060
|
}, {
|
|
1022
1061
|
maxIterations?: number | undefined;
|
|
1023
1062
|
maxIterationsOneShot?: number | undefined;
|
|
1063
|
+
maxTokensPerTurn?: number | undefined;
|
|
1024
1064
|
autoApprove?: boolean | undefined;
|
|
1025
1065
|
planMode?: boolean | undefined;
|
|
1026
1066
|
}>>;
|
|
@@ -1068,16 +1108,26 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1068
1108
|
maxTokens: z.ZodDefault<z.ZodNumber>;
|
|
1069
1109
|
/** Compact once the history estimate exceeds this fraction of maxTokens. */
|
|
1070
1110
|
compactThreshold: z.ZodDefault<z.ZodNumber>;
|
|
1111
|
+
/**
|
|
1112
|
+
* Fixed token allowance for request payload that `estimateTokens` never
|
|
1113
|
+
* sees — the system prompt and every tool's JSON schema — added to the
|
|
1114
|
+
* measured history before the threshold test so the trigger reflects the
|
|
1115
|
+
* real request size, not just the visible messages. Roughly the size of
|
|
1116
|
+
* the built system prompt plus the default tool catalogue today.
|
|
1117
|
+
*/
|
|
1118
|
+
reserveTokens: z.ZodDefault<z.ZodNumber>;
|
|
1071
1119
|
/** Most-recent messages always kept verbatim (a floor; the cut rounds up to
|
|
1072
1120
|
* a clean turn boundary). */
|
|
1073
1121
|
keepRecentMessages: z.ZodDefault<z.ZodNumber>;
|
|
1074
1122
|
}, "strict", z.ZodTypeAny, {
|
|
1075
1123
|
maxTokens: number;
|
|
1076
1124
|
compactThreshold: number;
|
|
1125
|
+
reserveTokens: number;
|
|
1077
1126
|
keepRecentMessages: number;
|
|
1078
1127
|
}, {
|
|
1079
1128
|
maxTokens?: number | undefined;
|
|
1080
1129
|
compactThreshold?: number | undefined;
|
|
1130
|
+
reserveTokens?: number | undefined;
|
|
1081
1131
|
keepRecentMessages?: number | undefined;
|
|
1082
1132
|
}>>;
|
|
1083
1133
|
approval: z.ZodDefault<z.ZodObject<{
|
|
@@ -1746,6 +1796,22 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1746
1796
|
approval: {
|
|
1747
1797
|
mode: "prompt";
|
|
1748
1798
|
};
|
|
1799
|
+
mcp: {
|
|
1800
|
+
startupTimeout: number;
|
|
1801
|
+
requestTimeout: number;
|
|
1802
|
+
servers: Record<string, {
|
|
1803
|
+
args: string[];
|
|
1804
|
+
env: Record<string, string>;
|
|
1805
|
+
command?: string | undefined;
|
|
1806
|
+
credentialRef?: string | undefined;
|
|
1807
|
+
url?: string | undefined;
|
|
1808
|
+
headers?: Record<string, string> | undefined;
|
|
1809
|
+
}>;
|
|
1810
|
+
enabled: boolean;
|
|
1811
|
+
maxToolsPerServer: number;
|
|
1812
|
+
maxDescriptionChars: number;
|
|
1813
|
+
maxSchemaBytes: number;
|
|
1814
|
+
};
|
|
1749
1815
|
subagent: {
|
|
1750
1816
|
maxDepth: number;
|
|
1751
1817
|
maxConcurrency: number;
|
|
@@ -1768,6 +1834,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1768
1834
|
agent: {
|
|
1769
1835
|
maxIterations: number;
|
|
1770
1836
|
maxIterationsOneShot: number;
|
|
1837
|
+
maxTokensPerTurn: number;
|
|
1771
1838
|
autoApprove: boolean;
|
|
1772
1839
|
planMode: boolean;
|
|
1773
1840
|
};
|
|
@@ -1783,6 +1850,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1783
1850
|
context: {
|
|
1784
1851
|
maxTokens: number;
|
|
1785
1852
|
compactThreshold: number;
|
|
1853
|
+
reserveTokens: number;
|
|
1786
1854
|
keepRecentMessages: number;
|
|
1787
1855
|
};
|
|
1788
1856
|
index: {
|
|
@@ -1827,22 +1895,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1827
1895
|
map: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">>;
|
|
1828
1896
|
default?: "kavi" | "vaani" | "mira" | undefined;
|
|
1829
1897
|
};
|
|
1830
|
-
mcp: {
|
|
1831
|
-
startupTimeout: number;
|
|
1832
|
-
requestTimeout: number;
|
|
1833
|
-
servers: Record<string, {
|
|
1834
|
-
args: string[];
|
|
1835
|
-
env: Record<string, string>;
|
|
1836
|
-
command?: string | undefined;
|
|
1837
|
-
credentialRef?: string | undefined;
|
|
1838
|
-
url?: string | undefined;
|
|
1839
|
-
headers?: Record<string, string> | undefined;
|
|
1840
|
-
}>;
|
|
1841
|
-
enabled: boolean;
|
|
1842
|
-
maxToolsPerServer: number;
|
|
1843
|
-
maxDescriptionChars: number;
|
|
1844
|
-
maxSchemaBytes: number;
|
|
1845
|
-
};
|
|
1846
1898
|
web: {
|
|
1847
1899
|
provider: "tavily";
|
|
1848
1900
|
timeoutMs: number;
|
|
@@ -1899,6 +1951,22 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1899
1951
|
approval?: {
|
|
1900
1952
|
mode?: "prompt" | undefined;
|
|
1901
1953
|
} | undefined;
|
|
1954
|
+
mcp?: {
|
|
1955
|
+
startupTimeout?: number | undefined;
|
|
1956
|
+
requestTimeout?: number | undefined;
|
|
1957
|
+
servers?: Record<string, {
|
|
1958
|
+
command?: string | undefined;
|
|
1959
|
+
credentialRef?: string | undefined;
|
|
1960
|
+
url?: string | undefined;
|
|
1961
|
+
args?: string[] | undefined;
|
|
1962
|
+
env?: Record<string, string> | undefined;
|
|
1963
|
+
headers?: Record<string, string> | undefined;
|
|
1964
|
+
}> | undefined;
|
|
1965
|
+
enabled?: boolean | undefined;
|
|
1966
|
+
maxToolsPerServer?: number | undefined;
|
|
1967
|
+
maxDescriptionChars?: number | undefined;
|
|
1968
|
+
maxSchemaBytes?: number | undefined;
|
|
1969
|
+
} | undefined;
|
|
1902
1970
|
subagent?: {
|
|
1903
1971
|
maxDepth?: number | undefined;
|
|
1904
1972
|
maxConcurrency?: number | undefined;
|
|
@@ -1921,6 +1989,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1921
1989
|
agent?: {
|
|
1922
1990
|
maxIterations?: number | undefined;
|
|
1923
1991
|
maxIterationsOneShot?: number | undefined;
|
|
1992
|
+
maxTokensPerTurn?: number | undefined;
|
|
1924
1993
|
autoApprove?: boolean | undefined;
|
|
1925
1994
|
planMode?: boolean | undefined;
|
|
1926
1995
|
} | undefined;
|
|
@@ -1936,6 +2005,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1936
2005
|
context?: {
|
|
1937
2006
|
maxTokens?: number | undefined;
|
|
1938
2007
|
compactThreshold?: number | undefined;
|
|
2008
|
+
reserveTokens?: number | undefined;
|
|
1939
2009
|
keepRecentMessages?: number | undefined;
|
|
1940
2010
|
} | undefined;
|
|
1941
2011
|
index?: {
|
|
@@ -1980,22 +2050,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1980
2050
|
map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
|
|
1981
2051
|
default?: "kavi" | "vaani" | "mira" | undefined;
|
|
1982
2052
|
} | undefined;
|
|
1983
|
-
mcp?: {
|
|
1984
|
-
startupTimeout?: number | undefined;
|
|
1985
|
-
requestTimeout?: number | undefined;
|
|
1986
|
-
servers?: Record<string, {
|
|
1987
|
-
command?: string | undefined;
|
|
1988
|
-
credentialRef?: string | undefined;
|
|
1989
|
-
url?: string | undefined;
|
|
1990
|
-
args?: string[] | undefined;
|
|
1991
|
-
env?: Record<string, string> | undefined;
|
|
1992
|
-
headers?: Record<string, string> | undefined;
|
|
1993
|
-
}> | undefined;
|
|
1994
|
-
enabled?: boolean | undefined;
|
|
1995
|
-
maxToolsPerServer?: number | undefined;
|
|
1996
|
-
maxDescriptionChars?: number | undefined;
|
|
1997
|
-
maxSchemaBytes?: number | undefined;
|
|
1998
|
-
} | undefined;
|
|
1999
2053
|
web?: {
|
|
2000
2054
|
provider?: "tavily" | undefined;
|
|
2001
2055
|
timeoutMs?: number | undefined;
|
package/dist/config/schema.js
CHANGED
|
@@ -41,6 +41,19 @@ export const AgentConfigSchema = z
|
|
|
41
41
|
* `maxIterations` — a user may deliberately set it lower or higher.
|
|
42
42
|
*/
|
|
43
43
|
maxIterationsOneShot: z.number().int().positive().default(40),
|
|
44
|
+
/**
|
|
45
|
+
* Optional hard cap on combined input+output tokens for a SINGLE user turn
|
|
46
|
+
* — a last-resort runaway guard, not cost control (cumulative session cost
|
|
47
|
+
* is C.22's concern). OFF by default (0 = off): today's behavior is
|
|
48
|
+
* preserved exactly and no turn is ever token-stopped. When set, a turn that
|
|
49
|
+
* crosses the cap stops at the next turn boundary with a coherent partial
|
|
50
|
+
* history (`stop: "budget"`); in one-shot that fails loud (exit 20), while
|
|
51
|
+
* interactively the human simply continues. Compaction is the everyday
|
|
52
|
+
* pressure valve for context — this only bounds a genuinely runaway turn, so
|
|
53
|
+
* a hostile hard stop mid-work stays opt-in. Reset each `send`, never
|
|
54
|
+
* cumulative across turns.
|
|
55
|
+
*/
|
|
56
|
+
maxTokensPerTurn: z.number().int().nonnegative().default(0),
|
|
44
57
|
/** Skip per-action confirmation prompts. */
|
|
45
58
|
autoApprove: z.boolean().default(false),
|
|
46
59
|
/** Plan mode: propose a plan for approval before executing (C.31, opt-in). */
|
|
@@ -81,6 +94,14 @@ export const ContextConfigSchema = z
|
|
|
81
94
|
maxTokens: z.number().int().positive().default(100000),
|
|
82
95
|
/** Compact once the history estimate exceeds this fraction of maxTokens. */
|
|
83
96
|
compactThreshold: z.number().min(0).max(1).default(0.75),
|
|
97
|
+
/**
|
|
98
|
+
* Fixed token allowance for request payload that `estimateTokens` never
|
|
99
|
+
* sees — the system prompt and every tool's JSON schema — added to the
|
|
100
|
+
* measured history before the threshold test so the trigger reflects the
|
|
101
|
+
* real request size, not just the visible messages. Roughly the size of
|
|
102
|
+
* the built system prompt plus the default tool catalogue today.
|
|
103
|
+
*/
|
|
104
|
+
reserveTokens: z.number().int().nonnegative().default(4500),
|
|
84
105
|
/** Most-recent messages always kept verbatim (a floor; the cut rounds up to
|
|
85
106
|
* a clean turn boundary). */
|
|
86
107
|
keepRecentMessages: z.number().int().positive().default(6),
|
|
@@ -24,6 +24,15 @@ export declare function configParse(path: string, underlying?: unknown): CruxyEr
|
|
|
24
24
|
export declare function configInvalid(issues: string, path?: string): CruxyError;
|
|
25
25
|
export declare function authMissingKey(provider: string, envVar: string): CruxyError;
|
|
26
26
|
export declare function authInvalid(underlying?: unknown): CruxyError;
|
|
27
|
+
/**
|
|
28
|
+
* A credential could not be persisted with owner-only permissions, so it was
|
|
29
|
+
* NOT written (C.27c). Chiefly a Windows case: the store's ACL could not be
|
|
30
|
+
* restricted to the current user (non-NTFS filesystem, `icacls`/SID
|
|
31
|
+
* unavailable), and we refuse to leave a secret at inheritable permissions while
|
|
32
|
+
* claiming otherwise. Provider keys point at the env-var fallback; MCP tokens
|
|
33
|
+
* have no such fallback, so the message says so plainly.
|
|
34
|
+
*/
|
|
35
|
+
export declare function credentialsUnprotected(kind: "provider" | "mcp", path: string, underlying?: unknown): CruxyError;
|
|
27
36
|
export declare function gatewayUnreachable(underlying?: unknown): CruxyError;
|
|
28
37
|
export declare function apiError(underlying?: unknown): CruxyError;
|
|
29
38
|
export declare function apiRateLimit(underlying?: unknown): CruxyError;
|
|
@@ -142,6 +142,33 @@ export function authInvalid(underlying) {
|
|
|
142
142
|
underlying,
|
|
143
143
|
});
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* A credential could not be persisted with owner-only permissions, so it was
|
|
147
|
+
* NOT written (C.27c). Chiefly a Windows case: the store's ACL could not be
|
|
148
|
+
* restricted to the current user (non-NTFS filesystem, `icacls`/SID
|
|
149
|
+
* unavailable), and we refuse to leave a secret at inheritable permissions while
|
|
150
|
+
* claiming otherwise. Provider keys point at the env-var fallback; MCP tokens
|
|
151
|
+
* have no such fallback, so the message says so plainly.
|
|
152
|
+
*/
|
|
153
|
+
export function credentialsUnprotected(kind, path, underlying) {
|
|
154
|
+
const nextSteps = kind === "provider"
|
|
155
|
+
? [
|
|
156
|
+
"set the key via the CRUXY_API_KEY environment variable instead — env is never written to disk and always wins",
|
|
157
|
+
"or store it on a filesystem that supports owner-only permissions (NTFS, not FAT/exFAT)",
|
|
158
|
+
]
|
|
159
|
+
: [
|
|
160
|
+
"an MCP bearer token has no environment fallback — it can only live in the owner-only store",
|
|
161
|
+
"store it on a filesystem that supports owner-only permissions (NTFS, not FAT/exFAT)",
|
|
162
|
+
];
|
|
163
|
+
return new CruxyError({
|
|
164
|
+
code: ErrorCode.CredentialsUnprotected,
|
|
165
|
+
title: `refusing to write a credential that cannot be made owner-only: ${path}`,
|
|
166
|
+
cause: scrubbedMessageOf(underlying),
|
|
167
|
+
nextSteps,
|
|
168
|
+
underlying,
|
|
169
|
+
meta: { path, kind },
|
|
170
|
+
});
|
|
171
|
+
}
|
|
145
172
|
// ── network (exit 5) ──────────────────────────────────────────────────────────
|
|
146
173
|
export function gatewayUnreachable(underlying) {
|
|
147
174
|
return new CruxyError({
|
package/dist/errors/types.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export declare const ErrorCode: {
|
|
|
29
29
|
readonly AuthMissingKey: "CRUXY_E_AUTH_MISSING_KEY";
|
|
30
30
|
readonly AuthInvalid: "CRUXY_E_AUTH_INVALID";
|
|
31
31
|
readonly ForgeAuth: "CRUXY_E_FORGE_AUTH";
|
|
32
|
+
/** A credential could not be persisted with owner-only permissions (C.27c) —
|
|
33
|
+
* e.g. on Windows the store's ACL could not be restricted to the current user
|
|
34
|
+
* (non-NTFS filesystem, `icacls`/SID unavailable). Refused loudly rather than
|
|
35
|
+
* written world-inheritable: a secret is never persisted at permissions we
|
|
36
|
+
* could not verify as owner-only. */
|
|
37
|
+
readonly CredentialsUnprotected: "CRUXY_E_CREDENTIALS_UNPROTECTED";
|
|
32
38
|
readonly GatewayUnreachable: "CRUXY_E_GATEWAY_UNREACHABLE";
|
|
33
39
|
readonly GitPushFailed: "CRUXY_E_GIT_PUSH_FAILED";
|
|
34
40
|
readonly Api: "CRUXY_E_API";
|
package/dist/errors/types.js
CHANGED
|
@@ -33,6 +33,12 @@ export const ErrorCode = {
|
|
|
33
33
|
AuthMissingKey: "CRUXY_E_AUTH_MISSING_KEY",
|
|
34
34
|
AuthInvalid: "CRUXY_E_AUTH_INVALID",
|
|
35
35
|
ForgeAuth: "CRUXY_E_FORGE_AUTH",
|
|
36
|
+
/** A credential could not be persisted with owner-only permissions (C.27c) —
|
|
37
|
+
* e.g. on Windows the store's ACL could not be restricted to the current user
|
|
38
|
+
* (non-NTFS filesystem, `icacls`/SID unavailable). Refused loudly rather than
|
|
39
|
+
* written world-inheritable: a secret is never persisted at permissions we
|
|
40
|
+
* could not verify as owner-only. */
|
|
41
|
+
CredentialsUnprotected: "CRUXY_E_CREDENTIALS_UNPROTECTED",
|
|
36
42
|
// network (exit 5)
|
|
37
43
|
GatewayUnreachable: "CRUXY_E_GATEWAY_UNREACHABLE",
|
|
38
44
|
GitPushFailed: "CRUXY_E_GIT_PUSH_FAILED",
|
|
@@ -249,6 +255,7 @@ const EXIT_CODES = {
|
|
|
249
255
|
[ErrorCode.AuthMissingKey]: 4,
|
|
250
256
|
[ErrorCode.AuthInvalid]: 4,
|
|
251
257
|
[ErrorCode.ForgeAuth]: 4,
|
|
258
|
+
[ErrorCode.CredentialsUnprotected]: 4,
|
|
252
259
|
[ErrorCode.GatewayUnreachable]: 5,
|
|
253
260
|
[ErrorCode.GitPushFailed]: 5,
|
|
254
261
|
[ErrorCode.Api]: 6,
|
package/dist/indexing/service.js
CHANGED
|
@@ -131,7 +131,10 @@ async function openStore(root, kind, logger) {
|
|
|
131
131
|
// an in-memory index with a warning (no quality loss, just no persistence).
|
|
132
132
|
if (kind === "sqlite")
|
|
133
133
|
throw indexStoreUnavailable(err);
|
|
134
|
-
logger.warn(`
|
|
134
|
+
logger.warn(`persistent index unavailable (${err.message}); using an ephemeral ` +
|
|
135
|
+
`in-memory index — it reindexes from scratch each session. To enable persistence, ` +
|
|
136
|
+
`let better-sqlite3 build (install your platform's C/C++ build tools) or run on Node ≥22 ` +
|
|
137
|
+
`(which ships a prebuilt binary).`);
|
|
135
138
|
return { store: new InMemoryVectorStore(), storePath: null };
|
|
136
139
|
}
|
|
137
140
|
}
|
package/dist/jobs/manager.js
CHANGED
|
@@ -2,7 +2,8 @@ import { runAgent } from "../agent/loop.js";
|
|
|
2
2
|
import { ApprovalService, classify, serializeGate, } from "../approval/index.js";
|
|
3
3
|
import { CheckpointGate, withCheckpointGate } from "../checkpoint/index.js";
|
|
4
4
|
import { CruxyError, ErrorCode, approvalRequired, jobLimitExceeded, jobNotFound, jobsDisabled, messageOf, } from "../errors/index.js";
|
|
5
|
-
import { Budget, resolveBudget
|
|
5
|
+
import { Budget, resolveBudget } from "../agent/budget.js";
|
|
6
|
+
import { scopeRegistry } from "../subagent/index.js";
|
|
6
7
|
import { Workspace } from "../workspace/index.js";
|
|
7
8
|
import { LogBuffer } from "./log-buffer.js";
|
|
8
9
|
import { JobLogRenderer } from "./log-renderer.js";
|
package/dist/memory/types.d.ts
CHANGED
|
@@ -45,14 +45,14 @@ export declare const MemoryEntrySchema: z.ZodObject<{
|
|
|
45
45
|
/** ISO 8601 timestamp the entry was recorded. */
|
|
46
46
|
createdAt: z.ZodString;
|
|
47
47
|
}, "strict", z.ZodTypeAny, {
|
|
48
|
-
id: string;
|
|
49
48
|
kind: "fact" | "decision" | "preference";
|
|
49
|
+
id: string;
|
|
50
50
|
createdAt: string;
|
|
51
51
|
content: string;
|
|
52
52
|
scope: "project" | "user";
|
|
53
53
|
}, {
|
|
54
|
-
id: string;
|
|
55
54
|
kind: "fact" | "decision" | "preference";
|
|
55
|
+
id: string;
|
|
56
56
|
createdAt: string;
|
|
57
57
|
content: string;
|
|
58
58
|
scope: "project" | "user";
|
package/dist/subagent/index.d.ts
CHANGED
package/dist/subagent/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import { runAgent } from "../agent/loop.js";
|
|
3
3
|
import { CruxyError, ErrorCode, messageOf, subagentDepthExceeded, subagentScopeOverlap, } from "../errors/index.js";
|
|
4
4
|
import { Workspace } from "../workspace/index.js";
|
|
5
|
-
import { Budget, resolveBudget } from "
|
|
5
|
+
import { Budget, resolveBudget } from "../agent/budget.js";
|
|
6
6
|
import { scopeRegistry, SUBAGENT_WRITE_TOOLS } from "./registry-scope.js";
|
|
7
7
|
import { Semaphore } from "./semaphore.js";
|
|
8
8
|
import { makeSpawnSubagentTool } from "./spawn-tool.js";
|
package/dist/subagent/types.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Usage } from "@cruxy/sdk";
|
|
2
2
|
import type { TaskClass } from "../routing/index.js";
|
|
3
|
+
import type { BudgetLimits } from "../agent/budget.js";
|
|
4
|
+
export type { BudgetLimits };
|
|
3
5
|
/**
|
|
4
6
|
* Types for subagent orchestration (C.14): the main agent delegates a bounded
|
|
5
7
|
* subtask to a child agent that runs the SAME loop with its own fresh history,
|
|
@@ -14,19 +16,6 @@ import type { TaskClass } from "../routing/index.js";
|
|
|
14
16
|
* dressed up as `done`.
|
|
15
17
|
*/
|
|
16
18
|
export type SubagentStatus = "done" | "budget-exceeded" | "failed" | "cancelled";
|
|
17
|
-
/**
|
|
18
|
-
* Hard caps a subagent runs under. `maxIterations` and `maxTokens` are always
|
|
19
|
-
* finite — a subagent is bounded by construction; `timeoutMs` is an optional
|
|
20
|
-
* wall-clock backstop on top.
|
|
21
|
-
*/
|
|
22
|
-
export interface BudgetLimits {
|
|
23
|
-
/** Cap on the subagent's model turns. */
|
|
24
|
-
maxIterations: number;
|
|
25
|
-
/** Cap on the subagent's combined input+output tokens. */
|
|
26
|
-
maxTokens: number;
|
|
27
|
-
/** Optional wall-clock cap in milliseconds. */
|
|
28
|
-
timeoutMs?: number;
|
|
29
|
-
}
|
|
30
19
|
/** A spawn request: the bounded task plus optional scope/budget narrowing. */
|
|
31
20
|
export interface SubagentSpec {
|
|
32
21
|
/** The complete, self-contained subtask the subagent should perform. */
|
package/dist/testing/runner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { killTree, spawnTree } from "../utils/process-tree.js";
|
|
2
2
|
import { parseFailures } from "./parse.js";
|
|
3
3
|
/**
|
|
4
4
|
* The shipped {@link TestRunner}: spawn the command via the system shell (the
|
|
@@ -15,11 +15,10 @@ export class CommandTestRunner {
|
|
|
15
15
|
const capture = new TailCapture(opts.captureBytes);
|
|
16
16
|
let child;
|
|
17
17
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
});
|
|
18
|
+
// Same killable-tree discipline as run_command: `spawnTree` groups the
|
|
19
|
+
// shell (POSIX process group / win32 OS tree) so a timeout can reap the
|
|
20
|
+
// whole tree via `killTree`.
|
|
21
|
+
child = spawnTree(command, [], { shell: true, cwd: opts.cwd });
|
|
23
22
|
}
|
|
24
23
|
catch (err) {
|
|
25
24
|
resolve(failed(null, err.message, startedAt));
|
|
@@ -111,14 +110,3 @@ export class TailCapture {
|
|
|
111
110
|
return this.truncated ? `… [earlier output truncated]\n${body}` : body;
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
|
-
/** Kill the whole process group (POSIX; matches run_command's behavior). */
|
|
115
|
-
function killTree(pid) {
|
|
116
|
-
if (pid === undefined)
|
|
117
|
-
return;
|
|
118
|
-
try {
|
|
119
|
-
process.kill(-pid, "SIGKILL");
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
// Already exited, or no group — nothing to kill.
|
|
123
|
-
}
|
|
124
|
-
}
|
package/dist/tools/shell/exec.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { killTree, spawnTree } from "../../utils/process-tree.js";
|
|
2
2
|
/**
|
|
3
3
|
* Gate `command` through `ctx.requestApproval`, then — only if allowed — execute
|
|
4
4
|
* it via the sandbox (when `ctx.sandbox` is set) or the bounded host spawn. A
|
|
@@ -50,9 +50,10 @@ async function runSandboxed(command, ctx) {
|
|
|
50
50
|
function runBounded(command, ctx) {
|
|
51
51
|
const { timeoutMs, maxOutputBytes } = ctx.config.shell;
|
|
52
52
|
return new Promise((resolve) => {
|
|
53
|
-
// `
|
|
54
|
-
//
|
|
55
|
-
|
|
53
|
+
// `spawnTree` makes the child the head of a killable tree (its own process
|
|
54
|
+
// group on POSIX; the OS parent-PID tree on win32) so the whole tree — the
|
|
55
|
+
// shell plus anything it spawns — can be killed on timeout via `killTree`.
|
|
56
|
+
const child = spawnTree(command, [], { shell: true, cwd: ctx.cwd });
|
|
56
57
|
const chunks = [];
|
|
57
58
|
let captured = 0;
|
|
58
59
|
let truncated = false;
|
|
@@ -150,18 +151,3 @@ function runBounded(command, ctx) {
|
|
|
150
151
|
});
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
|
-
/**
|
|
154
|
-
* Kill the command's entire process group. POSIX-specific (negative pid targets
|
|
155
|
-
* the group); fine on our darwin/linux targets. Swallows errors — the process
|
|
156
|
-
* may already be gone.
|
|
157
|
-
*/
|
|
158
|
-
function killTree(pid) {
|
|
159
|
-
if (pid === undefined)
|
|
160
|
-
return;
|
|
161
|
-
try {
|
|
162
|
-
process.kill(-pid, "SIGKILL");
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
// Already exited, or no group — nothing to kill.
|
|
166
|
-
}
|
|
167
|
-
}
|