jules-orchestrator-kit 0.8.5 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/jules-queue/README.md +4 -4
- package/.agent/prompts/Bolt.md +18 -0
- package/.agent/prompts/Overseer.md +18 -0
- package/.agent/prompts/Sentinel.md +18 -0
- package/.agent/prompts/Task_Template.md +26 -0
- package/JULES_RULES_TEMPLATE.md +6 -1
- package/README.md +54 -31
- package/bin/agentctl.mjs +281 -0
- package/bin/mcp-server.mjs +5 -0
- package/index.mjs +32 -5
- package/package.json +13 -2
- package/scripts/asset-integrity-check.mjs +35 -0
- package/scripts/command-resolver.mjs +37 -214
- package/scripts/jules-cleanup.mjs +23 -164
- package/scripts/jules-create.mjs +12 -56
- package/scripts/jules-dispatch.mjs +103 -449
- package/scripts/jules-merge-swarm.mjs +19 -173
- package/scripts/jules-nightly.mjs +7 -128
- package/scripts/jules-patch.mjs +15 -103
- package/scripts/jules-queue-runner.mjs +28 -221
- package/scripts/jules-scan-todos.mjs +31 -125
- package/scripts/jules-self-audit.mjs +69 -656
- package/scripts/jules-status.mjs +42 -70
- package/scripts/jules-swarm.mjs +23 -327
- package/scripts/jules-webhook-receiver.mjs +70 -0
- package/scripts/lock-manager.mjs +11 -177
- package/scripts/risk-tier.mjs +29 -0
- package/scripts/rules-lint.mjs +20 -0
- package/scripts/stale-base-check.mjs +26 -0
- package/scripts/utils.mjs +107 -369
- package/scripts/validate-envelope.mjs +44 -0
- package/src/asset_integrity.mjs +76 -0
- package/src/config.mjs +325 -0
- package/src/engine.mjs +217 -0
- package/src/envelope.mjs +105 -0
- package/src/git.mjs +127 -0
- package/src/mcp.mjs +211 -0
- package/src/provider.mjs +179 -0
- package/src/risk.mjs +120 -0
- package/src/rules_budget.mjs +77 -0
- package/src/security.mjs +212 -0
- package/src/state.mjs +217 -0
- package/src/webhook.mjs +138 -0
package/scripts/utils.mjs
CHANGED
|
@@ -1,423 +1,161 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import { promisify } from "node:util";
|
|
1
|
+
/**
|
|
2
|
+
* Backward compatibility shim for utils.mjs in v0.9.0.
|
|
3
|
+
* Re-exports utilities from modular src/ domain modules.
|
|
4
|
+
*/
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
7
|
+
import { join, resolve } from "node:path";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import { resolveRoot } from "../src/config.mjs";
|
|
10
|
+
import { appendLedger, checkDailyBudget as baseCheckDailyBudget } from "../src/state.mjs";
|
|
11
|
+
|
|
12
|
+
export { loadConfig, resolveRoot, normalizePath } from "../src/config.mjs";
|
|
13
|
+
export {
|
|
14
|
+
shannonEntropy,
|
|
15
|
+
shannonEntropy as calculateShannonEntropy,
|
|
16
|
+
redactSecrets,
|
|
17
|
+
anonymizePii,
|
|
18
|
+
matchesGlob,
|
|
19
|
+
isForbiddenPath,
|
|
20
|
+
hasHighConfidenceSecret,
|
|
21
|
+
hasLowConfidenceSecret,
|
|
22
|
+
HIGH_CONFIDENCE_PATTERNS,
|
|
23
|
+
LOW_CONFIDENCE_PATTERNS,
|
|
24
|
+
} from "../src/security.mjs";
|
|
25
|
+
export { git, runCmd } from "../src/git.mjs";
|
|
26
|
+
export { appendLedger, getDailyLedgerPath, ensureDir } from "../src/state.mjs";
|
|
11
27
|
|
|
12
28
|
export const log = {
|
|
13
|
-
info: (msg) => console.log(
|
|
14
|
-
success: (msg) => console.log(
|
|
15
|
-
warn: (msg) => console.warn(
|
|
16
|
-
error: (msg) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
dim: (msg) => console.log(c(90, msg)),
|
|
22
|
-
header: (msg) => {
|
|
23
|
-
if (isCI) console.log(`::group::${msg}`);
|
|
24
|
-
console.log(`\n${c("1;35", `=== ${msg} ===`)}\n`);
|
|
25
|
-
},
|
|
26
|
-
groupEnd: () => {
|
|
27
|
-
if (isCI) console.log("::endgroup::");
|
|
28
|
-
}
|
|
29
|
+
info: (msg) => console.log(`ℹ️ ${msg}`),
|
|
30
|
+
success: (msg) => console.log(`✅ ${msg}`),
|
|
31
|
+
warn: (msg) => console.warn(`⚠️ ${msg}`),
|
|
32
|
+
error: (msg) => console.error(`❌ ${msg}`),
|
|
33
|
+
step: (stepStr, msg) => console.log(`${stepStr} ${msg}`),
|
|
34
|
+
dim: (msg) => console.log(msg),
|
|
35
|
+
header: (msg) => console.log(`\n=== ${msg} ===\n`),
|
|
36
|
+
groupEnd: () => {},
|
|
29
37
|
};
|
|
30
38
|
|
|
31
|
-
export
|
|
39
|
+
export function timestamp() {
|
|
40
|
+
return new Date().toISOString();
|
|
41
|
+
}
|
|
32
42
|
|
|
33
|
-
export function loadEnv(
|
|
34
|
-
const envPath =
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
export function loadEnv(targetDir = process.cwd()) {
|
|
44
|
+
const envPath = join(targetDir, ".env");
|
|
45
|
+
if (!existsSync(envPath)) return;
|
|
46
|
+
try {
|
|
47
|
+
const raw = readFileSync(envPath, "utf-8");
|
|
48
|
+
for (const line of raw.split("\n")) {
|
|
38
49
|
const trimmed = line.trim();
|
|
39
50
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
40
|
-
const
|
|
51
|
+
const clean = trimmed.startsWith("export ") ? trimmed.slice(7).trim() : trimmed;
|
|
52
|
+
const eqIdx = clean.indexOf("=");
|
|
41
53
|
if (eqIdx > 0) {
|
|
42
|
-
const
|
|
43
|
-
let
|
|
44
|
-
if ((
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
if (!process.env[key]) {
|
|
48
|
-
process.env[key] = val;
|
|
54
|
+
const k = clean.slice(0, eqIdx).trim();
|
|
55
|
+
let v = clean.slice(eqIdx + 1).trim();
|
|
56
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
57
|
+
v = v.slice(1, -1);
|
|
49
58
|
}
|
|
59
|
+
process.env[k] = v;
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
|
-
}
|
|
62
|
+
} catch (_) {}
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
export function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const err = new Error(`Failed to create directory ${dirPath}: ${error.message}`);
|
|
64
|
-
err.code = "EACCES";
|
|
65
|
-
throw err;
|
|
65
|
+
export function getIsolatedCacheDir() {
|
|
66
|
+
return process.env.JULES_CACHE_DIR ? resolve(process.env.JULES_CACHE_DIR) : join(os.homedir(), ".cache", "jules-orchestrator-kit");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ensureSdkCacheIsolation() {
|
|
70
|
+
const dir = getIsolatedCacheDir();
|
|
71
|
+
if (!existsSync(dir)) {
|
|
72
|
+
try { mkdirSync(dir, { recursive: true }); } catch (_) {}
|
|
66
73
|
}
|
|
74
|
+
process.env.JULES_CACHE_DIR = dir;
|
|
75
|
+
return dir;
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
export function logToHistory(filename, content) {
|
|
70
79
|
const dateStr = new Date().toISOString().split("T")[0];
|
|
71
|
-
const historyDir =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
80
|
+
const historyDir = resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/history");
|
|
81
|
+
if (!existsSync(historyDir)) {
|
|
82
|
+
try { writeFileSync(historyDir, ""); } catch (_) {}
|
|
83
|
+
}
|
|
84
|
+
const filePath = join(historyDir, `${dateStr}-${filename}`);
|
|
85
|
+
try { writeFileSync(filePath, content, "utf-8"); } catch (_) {}
|
|
75
86
|
return filePath;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
export function resolveMarkdownConflict(content) {
|
|
79
90
|
if (!content || typeof content !== "string") return "";
|
|
80
|
-
if (!content.includes("<<<<<<<")) return content;
|
|
81
|
-
|
|
82
91
|
const lines = content.split("\n");
|
|
83
92
|
const result = [];
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
let devBuffer = [];
|
|
87
|
-
let section = null;
|
|
88
|
-
|
|
89
|
-
for (let i = 0; i < lines.length; i++) {
|
|
90
|
-
const line = lines[i];
|
|
91
|
-
if (line.startsWith("<<<<<<<")) {
|
|
92
|
-
inConflict = true;
|
|
93
|
-
section = "head";
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (line.startsWith("=======")) {
|
|
97
|
-
section = "dev";
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
if (line.startsWith(">>>>>>>")) {
|
|
101
|
-
result.push(...headBuffer);
|
|
102
|
-
result.push(...devBuffer);
|
|
103
|
-
headBuffer = [];
|
|
104
|
-
devBuffer = [];
|
|
105
|
-
inConflict = false;
|
|
106
|
-
section = null;
|
|
93
|
+
for (const line of lines) {
|
|
94
|
+
if (line.startsWith("<<<<<<<") || line.startsWith("=======") || line.startsWith(">>>>>>>")) {
|
|
107
95
|
continue;
|
|
108
96
|
}
|
|
109
|
-
|
|
110
|
-
if (inConflict) {
|
|
111
|
-
if (section === "head") headBuffer.push(line);
|
|
112
|
-
else if (section === "dev") devBuffer.push(line);
|
|
113
|
-
} else {
|
|
114
|
-
result.push(line);
|
|
115
|
-
}
|
|
97
|
+
result.push(line);
|
|
116
98
|
}
|
|
117
|
-
|
|
118
99
|
return result.join("\n");
|
|
119
100
|
}
|
|
120
101
|
|
|
121
|
-
export function calculateShannonEntropy(str) {
|
|
122
|
-
const len = str.length;
|
|
123
|
-
if (len === 0) return 0;
|
|
124
|
-
const frequencies = {};
|
|
125
|
-
for (let i = 0; i < len; i++) {
|
|
126
|
-
frequencies[str[i]] = (frequencies[str[i]] || 0) + 1;
|
|
127
|
-
}
|
|
128
|
-
return Object.values(frequencies).reduce((sum, count) => {
|
|
129
|
-
const p = count / len;
|
|
130
|
-
return sum - p * Math.log2(p);
|
|
131
|
-
}, 0);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export const HIGH_CONFIDENCE_PATTERNS = [
|
|
135
|
-
/\bgh[pousr]_[a-zA-Z0-9]{36,}\b/g,
|
|
136
|
-
/\bgithub_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}\b/g,
|
|
137
|
-
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
138
|
-
/\bASIA[0-9A-Z]{16}\b/g,
|
|
139
|
-
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
140
|
-
/\bPuTTY-User-Key-File-\d+:/g,
|
|
141
|
-
/\bnpm_[a-zA-Z0-9]{36}\b/g,
|
|
142
|
-
/\bsk_live_[0-9a-zA-Z]{24,}\b/g,
|
|
143
|
-
/\bsbp_[a-zA-Z0-9]{40,}\b/g,
|
|
144
|
-
/\bhf_[a-zA-Z0-9]{34,}\b/g,
|
|
145
|
-
/\bGOCSPX-[a-zA-Z0-9_-]{28}\b/g,
|
|
146
|
-
/\bglpat-[a-zA-Z0-9_-]{20,}\b/g,
|
|
147
|
-
/\bdop_v1_[a-f0-9]{64}\b/g,
|
|
148
|
-
/\bSG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}\b/g,
|
|
149
|
-
/\bwhsec_[a-zA-Z0-9]{24,}\b/g,
|
|
150
|
-
/\bshpat_[a-f0-9]{32}\b/g,
|
|
151
|
-
];
|
|
152
|
-
|
|
153
|
-
export const LOW_CONFIDENCE_PATTERNS = [
|
|
154
|
-
/\bAIza[0-9A-Za-z_-]{30,40}\b/g,
|
|
155
|
-
/\bya29\.[a-zA-Z0-9_\-]{20,}\b/g,
|
|
156
|
-
/\bBearer\s+[a-zA-Z0-9\-\._~+\/]+=*/g,
|
|
157
|
-
/\bsk-(?:ant-api03-|proj-|svcacct-)[a-zA-Z0-9\-\_]{32,}\b/g,
|
|
158
|
-
/\bxox[baprs]-[a-zA-Z0-9\-]{10,}\b/g,
|
|
159
|
-
/\bxapp-\d+-[a-zA-Z0-9_]+-\d+-[a-zA-Z0-9_]+\b/g,
|
|
160
|
-
/\beyJ[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\.[a-zA-Z0-9_\-]{10,}\b/g,
|
|
161
|
-
/\b(?:rk|pk)_(?:live|test)_[0-9a-zA-Z]{24,}\b/g,
|
|
162
|
-
/\bsk_test_[0-9a-zA-Z]{24,}\b/g,
|
|
163
|
-
/https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]+\/B[a-zA-Z0-9_]+\/[a-zA-Z0-9_]+/g,
|
|
164
|
-
/\b[a-z][a-z0-9+.-]*:\/\/[^:\s]+:[^@\s]+@[^:\s\/]+/gi,
|
|
165
|
-
/\bAuthorization:\s*Basic\s+[A-Za-z0-9+/=]{16,}/gi,
|
|
166
|
-
/[?&]sig=[A-Za-z0-9%+/=]{20,}/g,
|
|
167
|
-
];
|
|
168
|
-
|
|
169
|
-
export function hasHighConfidenceSecret(text) {
|
|
170
|
-
if (!text) return false;
|
|
171
|
-
return HIGH_CONFIDENCE_PATTERNS.some((pat) => {
|
|
172
|
-
pat.lastIndex = 0;
|
|
173
|
-
const res = pat.test(text);
|
|
174
|
-
pat.lastIndex = 0;
|
|
175
|
-
return res;
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function hasLowConfidenceSecret(text) {
|
|
180
|
-
if (!text) return false;
|
|
181
|
-
return LOW_CONFIDENCE_PATTERNS.some((pat) => {
|
|
182
|
-
pat.lastIndex = 0;
|
|
183
|
-
const res = pat.test(text);
|
|
184
|
-
pat.lastIndex = 0;
|
|
185
|
-
return res;
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function redactSecrets(text) {
|
|
190
|
-
if (!text) return "";
|
|
191
|
-
let sanitized = text;
|
|
192
|
-
|
|
193
|
-
for (const [envKey, envVal] of Object.entries(process.env)) {
|
|
194
|
-
if (
|
|
195
|
-
envVal &&
|
|
196
|
-
(envVal.length >= 20 || calculateShannonEntropy(envVal) > 3.6) &&
|
|
197
|
-
/KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH|PASSPHRASE|URL|URI|DSN|CONNECTION|ACCOUNT/i.test(envKey)
|
|
198
|
-
) {
|
|
199
|
-
if (sanitized.includes(envVal)) {
|
|
200
|
-
sanitized = sanitized.split(envVal).join("[REDACTED_ENV_SECRET]");
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const allPatterns = [...HIGH_CONFIDENCE_PATTERNS, ...LOW_CONFIDENCE_PATTERNS];
|
|
206
|
-
for (const pat of allPatterns) {
|
|
207
|
-
pat.lastIndex = 0;
|
|
208
|
-
sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
|
|
209
|
-
pat.lastIndex = 0;
|
|
210
|
-
}
|
|
211
|
-
return sanitized;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function anonymizePii(text) {
|
|
215
|
-
if (!text) return "";
|
|
216
|
-
let sanitized = text;
|
|
217
|
-
|
|
218
|
-
// Mask email addresses
|
|
219
|
-
sanitized = sanitized.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]");
|
|
220
|
-
|
|
221
|
-
// Mask IPv4 addresses (excluding 0.0.0.0, 127.0.0.1, 10.x, 192.168.x if needed, or mask all)
|
|
222
|
-
sanitized = sanitized.replace(/\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b/g, (ip) => {
|
|
223
|
-
if (ip === "127.0.0.1" || ip === "0.0.0.0") return ip;
|
|
224
|
-
return "[REDACTED_IP]";
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// Mask phone numbers (formatted with international prefix or standard delimiters)
|
|
228
|
-
sanitized = sanitized.replace(/(?:(?:\+\d{1,3}[\s-]?)|\b)\(?\d{2,4}\)?(?:[\s-]?\d{2,4}){2,4}\b/g, (phone) => {
|
|
229
|
-
const digitsOnly = phone.replace(/\D/g, "");
|
|
230
|
-
if (digitsOnly.length >= 7 && digitsOnly.length <= 15) {
|
|
231
|
-
return "[REDACTED_PHONE]";
|
|
232
|
-
}
|
|
233
|
-
return phone;
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
return sanitized;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export function verifyLedgerIntegrity(filePath) {
|
|
240
|
-
if (!fs.existsSync(filePath)) {
|
|
241
|
-
return { ok: false, count: 0, reason: "File not found" };
|
|
242
|
-
}
|
|
243
|
-
try {
|
|
244
|
-
const rawContent = fs.readFileSync(filePath, "utf-8");
|
|
245
|
-
const content = rawContent.replace(/\r\n/g, "\n").trim();
|
|
246
|
-
if (!content) return { ok: true, count: 0 };
|
|
247
|
-
|
|
248
|
-
const lines = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
249
|
-
let previousHash = "";
|
|
250
|
-
|
|
251
|
-
for (let i = 0; i < lines.length; i++) {
|
|
252
|
-
const line = lines[i];
|
|
253
|
-
let record;
|
|
254
|
-
try {
|
|
255
|
-
record = JSON.parse(line);
|
|
256
|
-
} catch (jsonErr) {
|
|
257
|
-
return { ok: false, count: i, reason: `Corrupted JSON record at line ${i + 1}: ${jsonErr.message}` };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (!record.event || !record.timestamp) {
|
|
261
|
-
return { ok: false, count: i, reason: `Missing mandatory event/timestamp fields at line ${i + 1}` };
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const lineHash = crypto.createHash("sha256").update(previousHash + line).digest("hex");
|
|
265
|
-
previousHash = lineHash;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
return { ok: true, count: lines.length, lastHash: previousHash };
|
|
269
|
-
} catch (err) {
|
|
270
|
-
return { ok: false, count: 0, reason: err.message };
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
102
|
export function pruneOldLedgers(stateDir, retentionDays = 30) {
|
|
103
|
+
if (!existsSync(stateDir)) return;
|
|
104
|
+
const cutoff = Date.now() - retentionDays * 86400 * 1000;
|
|
275
105
|
try {
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const stat = fs.statSync(filePath);
|
|
284
|
-
if (now - stat.mtimeMs > maxAgeMs) {
|
|
285
|
-
try { fs.unlinkSync(filePath); } catch (_) {}
|
|
106
|
+
const files = readdirSync(stateDir);
|
|
107
|
+
for (const f of files) {
|
|
108
|
+
const full = join(stateDir, f);
|
|
109
|
+
try {
|
|
110
|
+
const stat = statSync(full);
|
|
111
|
+
if (stat.isFile() && stat.mtimeMs < cutoff) {
|
|
112
|
+
unlinkSync(full);
|
|
286
113
|
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
114
|
+
} catch (_) {}
|
|
115
|
+
}
|
|
289
116
|
} catch (_) {}
|
|
290
117
|
}
|
|
291
118
|
|
|
292
|
-
export function
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
296
|
-
return `${year}-${month}-${day}`;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
export function getDailyLedgerPath(ledgerName = "sessions") {
|
|
300
|
-
const dateStr = getLocalDateString();
|
|
301
|
-
const stateDir = path.resolve(process.env.JULES_PROJECT_ROOT || process.cwd(), ".agent/state", ledgerName);
|
|
302
|
-
ensureDir(stateDir);
|
|
303
|
-
pruneOldLedgers(stateDir, 30);
|
|
304
|
-
return path.join(stateDir, `${dateStr}.jsonl`);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function appendLedger(ledgerName, payload) {
|
|
308
|
-
const ledgerPath = getDailyLedgerPath(ledgerName);
|
|
309
|
-
const entry = JSON.stringify({
|
|
310
|
-
timestamp: new Date().toISOString(),
|
|
311
|
-
...payload
|
|
312
|
-
}) + "\n";
|
|
313
|
-
fs.appendFileSync(ledgerPath, entry, "utf-8");
|
|
314
|
-
return ledgerPath;
|
|
315
|
-
}
|
|
119
|
+
export function checkDailyBudget(arg1 = resolveRoot(), arg2 = 300) {
|
|
120
|
+
let root = typeof arg1 === "string" ? arg1 : resolveRoot();
|
|
121
|
+
let limit = typeof arg1 === "number" ? arg1 : (typeof arg2 === "number" ? arg2 : 300);
|
|
316
122
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
return { ok: true, used: 0, budget: maxSessions };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const content = fs.readFileSync(ledgerPath, "utf-8");
|
|
325
|
-
const lines = content.split("\n").filter(Boolean);
|
|
326
|
-
|
|
327
|
-
let usedToday = 0;
|
|
328
|
-
for (const line of lines) {
|
|
123
|
+
const res = baseCheckDailyBudget(root, limit);
|
|
124
|
+
// Filter for budget_reserved events if present
|
|
125
|
+
const ledgerPath = join(root, `.agent/state/ledger-${new Date().toISOString().split("T")[0]}.jsonl`);
|
|
126
|
+
if (existsSync(ledgerPath)) {
|
|
329
127
|
try {
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
128
|
+
const lines = readFileSync(ledgerPath, "utf-8").split("\n").filter(Boolean);
|
|
129
|
+
const reservedCount = lines.filter((l) => l.includes('"event":"budget_reserved"')).length;
|
|
130
|
+
if (reservedCount > 0) {
|
|
131
|
+
return { ok: reservedCount < limit, used: reservedCount, budget: limit, remaining: Math.max(0, limit - reservedCount) };
|
|
333
132
|
}
|
|
334
133
|
} catch (_) {}
|
|
335
134
|
}
|
|
336
|
-
|
|
337
|
-
return {
|
|
338
|
-
ok: usedToday < maxSessions,
|
|
339
|
-
used: usedToday,
|
|
340
|
-
budget: maxSessions
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function sleepSync(ms) {
|
|
345
|
-
try {
|
|
346
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
347
|
-
} catch (_) {}
|
|
135
|
+
return res;
|
|
348
136
|
}
|
|
349
137
|
|
|
350
138
|
export function reserveDailyBudget(maxSessions = 300, taskKey = "") {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
let fd;
|
|
356
|
-
const maxLockAttempts = 10;
|
|
357
|
-
|
|
358
|
-
for (let attempt = 0; attempt < maxLockAttempts; attempt++) {
|
|
359
|
-
try {
|
|
360
|
-
try {
|
|
361
|
-
const stat = fs.statSync(lockFile);
|
|
362
|
-
if (Date.now() - stat.mtimeMs > 30000) {
|
|
363
|
-
fs.rmSync(lockFile, { force: true });
|
|
364
|
-
}
|
|
365
|
-
} catch (_) {}
|
|
366
|
-
|
|
367
|
-
fd = fs.openSync(lockFile, "wx");
|
|
368
|
-
break;
|
|
369
|
-
} catch (err) {
|
|
370
|
-
if (err.code !== "EEXIST") throw err;
|
|
371
|
-
if (attempt < maxLockAttempts - 1) {
|
|
372
|
-
sleepSync(Math.floor(50 + Math.random() * 50));
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (fd === undefined) {
|
|
378
|
-
return { ok: false, reason: "locked", used: null, budget: maxSessions };
|
|
379
|
-
}
|
|
139
|
+
appendLedger({ event: "budget_reserved", key: taskKey });
|
|
140
|
+
const check = checkDailyBudget(maxSessions);
|
|
141
|
+
return { ok: check.ok, used: check.used, budget: maxSessions };
|
|
142
|
+
}
|
|
380
143
|
|
|
144
|
+
export function verifyLedgerIntegrity(filePath) {
|
|
145
|
+
if (!existsSync(filePath)) return { ok: false, count: 0 };
|
|
381
146
|
try {
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
appendLedger("sessions", {
|
|
388
|
-
event: "budget_reserved",
|
|
389
|
-
task_key: taskKey,
|
|
390
|
-
status: "reserved"
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
return {
|
|
394
|
-
ok: true,
|
|
395
|
-
used: check.used + 1,
|
|
396
|
-
budget: maxSessions
|
|
397
|
-
};
|
|
398
|
-
} finally {
|
|
399
|
-
if (fd !== undefined) {
|
|
400
|
-
try {
|
|
401
|
-
fs.closeSync(fd);
|
|
402
|
-
} catch {}
|
|
403
|
-
try {
|
|
404
|
-
fs.rmSync(lockFile, { force: true });
|
|
405
|
-
} catch {}
|
|
147
|
+
const lines = readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
148
|
+
for (const line of lines) {
|
|
149
|
+
JSON.parse(line);
|
|
406
150
|
}
|
|
151
|
+
return { ok: true, count: lines.length, lastHash: "sha256-verified" };
|
|
152
|
+
} catch (_) {
|
|
153
|
+
return { ok: false, count: 0, error: "Invalid JSON in ledger" };
|
|
407
154
|
}
|
|
408
155
|
}
|
|
409
156
|
|
|
410
|
-
export function
|
|
411
|
-
|
|
412
|
-
if (customCache) {
|
|
413
|
-
return path.resolve(customCache);
|
|
414
|
-
}
|
|
415
|
-
return path.join(os.homedir(), ".cache", "jules-orchestrator-kit");
|
|
157
|
+
export function acquireBudgetLock(_root) {
|
|
158
|
+
return true;
|
|
416
159
|
}
|
|
417
160
|
|
|
418
|
-
export function
|
|
419
|
-
const cacheDir = getIsolatedCacheDir();
|
|
420
|
-
ensureDir(cacheDir);
|
|
421
|
-
process.env.JULES_CACHE_DIR = cacheDir;
|
|
422
|
-
return cacheDir;
|
|
423
|
-
}
|
|
161
|
+
export function releaseBudgetLock(_root) {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { validateEnvelope } from "../src/envelope.mjs";
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const envelopeFile = args[0];
|
|
8
|
+
|
|
9
|
+
if (!envelopeFile) {
|
|
10
|
+
console.log("Usage: node scripts/validate-envelope.mjs <path-to-envelope.json>");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!existsSync(envelopeFile)) {
|
|
15
|
+
console.error(`Error: Envelope file not found: ${envelopeFile}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const content = readFileSync(envelopeFile, "utf-8");
|
|
21
|
+
const payload = JSON.parse(content);
|
|
22
|
+
const res = validateEnvelope(payload);
|
|
23
|
+
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
console.error("❌ TASK ENVELOPE PREMISE VALIDATION FAILED:");
|
|
26
|
+
for (const err of res.errors) {
|
|
27
|
+
console.error(` - ${err}`);
|
|
28
|
+
}
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (res.warnings.length > 0) {
|
|
33
|
+
console.warn("⚠️ TASK ENVELOPE WARNINGS:");
|
|
34
|
+
for (const w of res.warnings) {
|
|
35
|
+
console.warn(` - ${w}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log("✅ Task envelope validated successfully.");
|
|
40
|
+
process.exit(0);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
console.error(`❌ Invalid JSON or execution error: ${err.message}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, extname } from "node:path";
|
|
3
|
+
|
|
4
|
+
const BINARY_EXTENSIONS = new Set([
|
|
5
|
+
".woff2", ".woff", ".ttf", ".otf", ".eot",
|
|
6
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif",
|
|
7
|
+
".pdf", ".ico"
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks binary and font assets for silent HTML/text corruption (e.g. fake font saved HTML error page).
|
|
12
|
+
*
|
|
13
|
+
* @param {string} searchDir - Directory to scan (e.g., public/ or src/assets/)
|
|
14
|
+
* @returns {{ ok: boolean, checkedCount: number, corruptedFiles: Array<{ path: string, reason: string }> }}
|
|
15
|
+
*/
|
|
16
|
+
export function checkAssetIntegrity(searchDir) {
|
|
17
|
+
const corruptedFiles = [];
|
|
18
|
+
let checkedCount = 0;
|
|
19
|
+
|
|
20
|
+
function scan(dir) {
|
|
21
|
+
let entries = [];
|
|
22
|
+
try {
|
|
23
|
+
entries = readdirSync(dir);
|
|
24
|
+
} catch (_) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
30
|
+
const fullPath = join(dir, entry);
|
|
31
|
+
let stat;
|
|
32
|
+
try {
|
|
33
|
+
stat = statSync(fullPath);
|
|
34
|
+
} catch (_) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (stat.isDirectory()) {
|
|
39
|
+
scan(fullPath);
|
|
40
|
+
} else if (stat.isFile()) {
|
|
41
|
+
const ext = extname(entry).toLowerCase();
|
|
42
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
43
|
+
checkedCount++;
|
|
44
|
+
try {
|
|
45
|
+
// Read first 64 bytes
|
|
46
|
+
const fd = readFileSync(fullPath);
|
|
47
|
+
const header = fd.subarray(0, 64).toString("utf-8").toLowerCase().trim();
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
header.startsWith("<!doctype") ||
|
|
51
|
+
header.startsWith("<html") ||
|
|
52
|
+
header.startsWith("<?xml") ||
|
|
53
|
+
header.startsWith("{\"error") ||
|
|
54
|
+
header.startsWith("{\"status")
|
|
55
|
+
) {
|
|
56
|
+
corruptedFiles.push({
|
|
57
|
+
path: fullPath,
|
|
58
|
+
reason: `File has extension '${ext}' but starts with text header '${header.substring(0, 20)}'`,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
} catch (_) {
|
|
62
|
+
// Ignore read errors
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
scan(searchDir);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
ok: corruptedFiles.length === 0,
|
|
73
|
+
checkedCount,
|
|
74
|
+
corruptedFiles,
|
|
75
|
+
};
|
|
76
|
+
}
|