jules-orchestrator-kit 0.8.1 → 0.9.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/.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/.agent/rules/dynamic-guardrails.json +0 -4
- package/.github/workflows/jules-audit.yml +16 -5
- package/JULES_RULES_TEMPLATE.md +7 -2
- package/README.md +113 -21
- package/bin/agentctl.mjs +274 -0
- package/bin/init.js +1 -1
- package/index.mjs +34 -6
- package/package.json +22 -6
- package/scripts/asset-integrity-check.mjs +35 -0
- package/scripts/command-resolver.mjs +37 -214
- package/scripts/jules-cleanup.mjs +23 -174
- package/scripts/jules-create.mjs +12 -51
- package/scripts/jules-dispatch.mjs +103 -349
- package/scripts/jules-merge-swarm.mjs +30 -0
- package/scripts/jules-nightly.mjs +7 -124
- package/scripts/jules-patch.mjs +23 -0
- package/scripts/jules-queue-runner.mjs +28 -203
- package/scripts/jules-scan-todos.mjs +31 -125
- package/scripts/jules-self-audit.mjs +76 -578
- package/scripts/jules-status.mjs +42 -65
- package/scripts/jules-swarm.mjs +27 -232
- package/scripts/jules-webhook-receiver.mjs +70 -0
- package/scripts/lock-manager.mjs +11 -170
- 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 +118 -252
- 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/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 +209 -0
- package/src/webhook.mjs +138 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -63
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -34
- package/.github/social-preview.png +0 -0
- package/.github/workflows/agent-scope-guard.yml +0 -57
- package/.github/workflows/jules-nightly.yml +0 -21
- package/.github/workflows/publish.yml +0 -24
package/src/security.mjs
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { normalizePath } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
export const HIGH_CONFIDENCE_PATTERNS = [
|
|
4
|
+
/\bghp_[A-Za-z0-9_]{36,255}\b/g,
|
|
5
|
+
/\bgho_[A-Za-z0-9_]{36,255}\b/g,
|
|
6
|
+
/\bghu_[A-Za-z0-9_]{36,255}\b/g,
|
|
7
|
+
/\bghs_[A-Za-z0-9_]{36,255}\b/g,
|
|
8
|
+
/\bghr_[A-Za-z0-9_]{36,255}\b/g,
|
|
9
|
+
|
|
10
|
+
/\bAKIA[0-9A-Z]{16}\b/g,
|
|
11
|
+
/\bASIA[0-9A-Z]{16}\b/g,
|
|
12
|
+
/\baws_secret_access_key\s*=\s*['"]?[A-Za-z0-9\/+=]{40}['"]?/g,
|
|
13
|
+
|
|
14
|
+
/-----BEGIN\s+(?:RSA|DSA|EC|OPENSSH|PRIVATE)\s+KEY-----[\s\S]*?-----END\s+(?:RSA|DSA|EC|OPENSSH|PRIVATE)\s+KEY-----/g,
|
|
15
|
+
/PuTTY-User-Key-File-[0-9]:[^\n]+/g,
|
|
16
|
+
|
|
17
|
+
/\bsk_live_[0-9a-zA-Z]{24,99}\b/g,
|
|
18
|
+
/\brk_live_[0-9a-zA-Z]{24,99}\b/g,
|
|
19
|
+
/\bnpm_[0-9a-zA-Z]{36}\b/g,
|
|
20
|
+
/\bglpat-[0-9a-zA-Z_-]{20,}\b/g,
|
|
21
|
+
/\bGOCSPX-[0-9a-zA-Z_-]{28}\b/g,
|
|
22
|
+
|
|
23
|
+
/\bAIzaSy[A-Za-z0-9_-]{33}\b/g,
|
|
24
|
+
|
|
25
|
+
/https:\/\/hooks\.slack\.com\/services\/T[A-Za-z0-9_]+\/B[A-Za-z0-9_]+\/[A-Za-z0-9_]+/g,
|
|
26
|
+
|
|
27
|
+
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/g,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
export const LOW_CONFIDENCE_PATTERNS = [
|
|
31
|
+
/Bearer\s+[A-Za-z0-9._~+/-]{10,}/gi,
|
|
32
|
+
/Authorization:\s*Bearer\s+[A-Za-z0-9._~+/-]+=*/gi,
|
|
33
|
+
/\bsk_test_[0-9a-zA-Z]{24,99}\b/g,
|
|
34
|
+
/(?:api[_-]?key|secret|password|passwd|token|auth[_-]?token)\s*[:=]\s*['"]([^'"]{8,128})['"]/gi,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function shannonEntropy(str) {
|
|
38
|
+
if (!str || typeof str !== "string") return 0;
|
|
39
|
+
const len = str.length;
|
|
40
|
+
const frequencies = {};
|
|
41
|
+
for (let i = 0; i < len; i++) {
|
|
42
|
+
const char = str[i];
|
|
43
|
+
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
44
|
+
}
|
|
45
|
+
let entropy = 0;
|
|
46
|
+
for (const char in frequencies) {
|
|
47
|
+
const p = frequencies[char] / len;
|
|
48
|
+
entropy -= p * Math.log2(p);
|
|
49
|
+
}
|
|
50
|
+
return entropy;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function hasHighConfidenceSecret(text) {
|
|
54
|
+
if (!text) return false;
|
|
55
|
+
return HIGH_CONFIDENCE_PATTERNS.some((pat) => {
|
|
56
|
+
pat.lastIndex = 0;
|
|
57
|
+
const res = pat.test(text);
|
|
58
|
+
pat.lastIndex = 0;
|
|
59
|
+
return res;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function hasLowConfidenceSecret(text) {
|
|
64
|
+
if (!text) return false;
|
|
65
|
+
return LOW_CONFIDENCE_PATTERNS.some((pat) => {
|
|
66
|
+
pat.lastIndex = 0;
|
|
67
|
+
const res = pat.test(text);
|
|
68
|
+
pat.lastIndex = 0;
|
|
69
|
+
return res;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function redactSecrets(text) {
|
|
74
|
+
if (!text) return "";
|
|
75
|
+
let sanitized = text;
|
|
76
|
+
|
|
77
|
+
for (const [envKey, envVal] of Object.entries(process.env)) {
|
|
78
|
+
if (
|
|
79
|
+
envVal &&
|
|
80
|
+
(envVal.length >= 20 || shannonEntropy(envVal) > 3.6) &&
|
|
81
|
+
/KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH|PASSPHRASE|URL|URI|DSN|CONNECTION|ACCOUNT/i.test(envKey)
|
|
82
|
+
) {
|
|
83
|
+
if (sanitized.includes(envVal)) {
|
|
84
|
+
sanitized = sanitized.split(envVal).join("[REDACTED_ENV_SECRET]");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const allPatterns = [...HIGH_CONFIDENCE_PATTERNS, ...LOW_CONFIDENCE_PATTERNS];
|
|
90
|
+
for (const pat of allPatterns) {
|
|
91
|
+
pat.lastIndex = 0;
|
|
92
|
+
sanitized = sanitized.replace(pat, "[REDACTED_BY_SECURITY_GATE]");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return sanitized;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function anonymizePii(text) {
|
|
99
|
+
if (!text) return "";
|
|
100
|
+
let sanitized = text;
|
|
101
|
+
|
|
102
|
+
sanitized = sanitized.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g, "[REDACTED_EMAIL]");
|
|
103
|
+
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) => {
|
|
104
|
+
if (ip === "127.0.0.1" || ip === "0.0.0.0") return ip;
|
|
105
|
+
return "[REDACTED_IP]";
|
|
106
|
+
});
|
|
107
|
+
sanitized = sanitized.replace(/(?:(?:\+\d{1,3}[\s-]?)|\b)\(?\d{2,4}\)?(?:[\s-]?\d{2,4}){2,4}\b/g, (phone) => {
|
|
108
|
+
const digitsOnly = phone.replace(/\D/g, "");
|
|
109
|
+
if (digitsOnly.length >= 7 && digitsOnly.length <= 15) {
|
|
110
|
+
return "[REDACTED_PHONE]";
|
|
111
|
+
}
|
|
112
|
+
return phone;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return sanitized;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function matchesGlob(filePath, globPattern) {
|
|
119
|
+
if (!filePath || !globPattern) return false;
|
|
120
|
+
const file = normalizePath(filePath);
|
|
121
|
+
const pattern = normalizePath(globPattern);
|
|
122
|
+
|
|
123
|
+
if (file === pattern) return true;
|
|
124
|
+
|
|
125
|
+
const parts = pattern.split("/");
|
|
126
|
+
const regexParts = parts.map((part) => {
|
|
127
|
+
if (part === "**") return "___GLOBSTAR___";
|
|
128
|
+
if (part === "*") return "[^/]+";
|
|
129
|
+
const escaped = part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
130
|
+
return escaped.replace(/\\\*/g, "[^/]*").replace(/\\\?/g, ".");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
let regexStr = regexParts.join("/");
|
|
134
|
+
regexStr = regexStr
|
|
135
|
+
.replace(/^___GLOBSTAR___\//g, "(?:.*/|^)")
|
|
136
|
+
.replace(/\/___GLOBSTAR___$/g, "(?:/.*|$)")
|
|
137
|
+
.replace(/\/___GLOBSTAR___\//g, "(?:/|/.+/|/)")
|
|
138
|
+
.replace(/___GLOBSTAR___/g, ".*");
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
return new RegExp(`^${regexStr}$`).test(file);
|
|
142
|
+
} catch (_) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isForbiddenPath(filePath, config = {}) {
|
|
148
|
+
const normFile = normalizePath(filePath);
|
|
149
|
+
const forbidden = config.scope?.deny || config.forbidden_paths || [];
|
|
150
|
+
return forbidden.some((pattern) => matchesGlob(normFile, pattern));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function checkScope(files = [], scope = {}, opts = {}) {
|
|
154
|
+
const violations = [];
|
|
155
|
+
const deny = scope.deny || [];
|
|
156
|
+
const allow = scope.allow || [];
|
|
157
|
+
const protect = scope.protect || [];
|
|
158
|
+
|
|
159
|
+
for (const rawFile of files) {
|
|
160
|
+
const file = normalizePath(rawFile);
|
|
161
|
+
|
|
162
|
+
const matchedDeny = deny.find((pat) => matchesGlob(file, pat));
|
|
163
|
+
if (matchedDeny) {
|
|
164
|
+
violations.push({ file, reason: `Forbidden path restriction matched pattern "${matchedDeny}"`, rule: "deny", pattern: matchedDeny });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (allow.length > 0) {
|
|
169
|
+
const isExplicitlyAllowed = allow.some((pat) => matchesGlob(file, pat));
|
|
170
|
+
if (!isExplicitlyAllowed) {
|
|
171
|
+
violations.push({ file, reason: "Path not included in allowed paths list", rule: "allow" });
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!opts.allowProtected) {
|
|
177
|
+
const matchedProtect = protect.find((pat) => matchesGlob(file, pat));
|
|
178
|
+
if (matchedProtect) {
|
|
179
|
+
violations.push({ file, reason: `Protected file modification restriction matched pattern "${matchedProtect}"`, rule: "protect", pattern: matchedProtect });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: violations.length === 0,
|
|
186
|
+
violations,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function scanDiff(diffTextStr = "") {
|
|
191
|
+
if (!diffTextStr) return { ok: true, findings: [] };
|
|
192
|
+
const addedLines = diffTextStr
|
|
193
|
+
.split("\n")
|
|
194
|
+
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
|
195
|
+
.map((line) => line.slice(1))
|
|
196
|
+
.join("\n");
|
|
197
|
+
|
|
198
|
+
const hasHigh = hasHighConfidenceSecret(addedLines);
|
|
199
|
+
const hasLow = !hasHigh && hasLowConfidenceSecret(addedLines);
|
|
200
|
+
const findings = [];
|
|
201
|
+
|
|
202
|
+
if (hasHigh) {
|
|
203
|
+
findings.push({ severity: "CRITICAL", type: "HIGH_CONFIDENCE_SECRET", description: "High-confidence secret pattern detected in added diff lines" });
|
|
204
|
+
} else if (hasLow) {
|
|
205
|
+
findings.push({ severity: "HIGH", type: "LOW_CONFIDENCE_SECRET", description: "Low-confidence secret or authorization token detected in added diff lines" });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
ok: !hasHigh && !hasLow,
|
|
210
|
+
findings,
|
|
211
|
+
};
|
|
212
|
+
}
|
package/src/state.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readFileSync,
|
|
3
|
+
appendFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
openSync,
|
|
8
|
+
writeSync,
|
|
9
|
+
closeSync,
|
|
10
|
+
unlinkSync,
|
|
11
|
+
} from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { resolveRoot } from "./config.mjs";
|
|
14
|
+
|
|
15
|
+
export function ensureDir(dirPath) {
|
|
16
|
+
if (!dirPath || typeof dirPath !== "string" || dirPath.includes("\0")) {
|
|
17
|
+
throw new Error("Invalid directory path provided");
|
|
18
|
+
}
|
|
19
|
+
if (!existsSync(dirPath)) {
|
|
20
|
+
mkdirSync(dirPath, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
return dirPath;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getQueueDir(rootOrOpts = resolveRoot()) {
|
|
26
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
27
|
+
const dir = join(root, ".agent/jules-queue");
|
|
28
|
+
ensureDir(dir);
|
|
29
|
+
return dir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getStateDir(rootOrOpts = resolveRoot()) {
|
|
33
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
34
|
+
const dir = join(root, ".agent/state");
|
|
35
|
+
ensureDir(dir);
|
|
36
|
+
return dir;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getDailyLedgerPath(rootOrOpts = resolveRoot()) {
|
|
40
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
41
|
+
const dateStr = new Date().toISOString().split("T")[0];
|
|
42
|
+
return join(getStateDir(root), `ledger-${dateStr}.jsonl`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Appends a structured audit event to the daily session ledger.
|
|
47
|
+
*/
|
|
48
|
+
export function appendLedger(entry, rootOrOpts = resolveRoot()) {
|
|
49
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
50
|
+
const filePath = getDailyLedgerPath(root);
|
|
51
|
+
const payload = {
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
...entry,
|
|
54
|
+
};
|
|
55
|
+
appendFileSync(filePath, JSON.stringify(payload) + "\n", "utf-8");
|
|
56
|
+
return payload;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readLedger(filePath) {
|
|
60
|
+
if (!filePath || !existsSync(filePath)) return [];
|
|
61
|
+
try {
|
|
62
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
63
|
+
return raw
|
|
64
|
+
.split("\n")
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.map((line) => {
|
|
67
|
+
const obj = JSON.parse(line);
|
|
68
|
+
return Object.freeze(obj);
|
|
69
|
+
});
|
|
70
|
+
} catch (_) {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Enforces a daily token budget.
|
|
77
|
+
*/
|
|
78
|
+
export function checkDailyBudget(arg1 = resolveRoot(), arg2 = 300) {
|
|
79
|
+
let root = typeof arg1 === "string" ? arg1 : resolveRoot();
|
|
80
|
+
let limit = typeof arg1 === "number" ? arg1 : typeof arg2 === "number" ? arg2 : 300;
|
|
81
|
+
|
|
82
|
+
const filePath = getDailyLedgerPath(root);
|
|
83
|
+
if (!existsSync(filePath)) {
|
|
84
|
+
return { ok: true, used: 0, budget: limit, remaining: limit };
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const content = readFileSync(filePath, "utf-8");
|
|
88
|
+
const lines = content.split("\n").filter(Boolean);
|
|
89
|
+
const count = lines.length;
|
|
90
|
+
return {
|
|
91
|
+
ok: count < limit,
|
|
92
|
+
used: count,
|
|
93
|
+
budget: limit,
|
|
94
|
+
remaining: Math.max(0, limit - count),
|
|
95
|
+
};
|
|
96
|
+
} catch (_) {
|
|
97
|
+
return { ok: true, used: 0, budget: limit, remaining: limit };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export async function withBudget(fn, root = resolveRoot(), limit = 300) {
|
|
102
|
+
const budget = checkDailyBudget(root, limit);
|
|
103
|
+
if (!budget.ok) {
|
|
104
|
+
throw new Error(`Daily budget exhausted (${budget.used}/${budget.budget} tasks executed)`);
|
|
105
|
+
}
|
|
106
|
+
return fn();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Atomic Advisory Lock File Manager with PID Vitality Check & Stale Lock Reaper (CWE-367 Fix).
|
|
111
|
+
*/
|
|
112
|
+
export function getLockDir(rootOrOpts = resolveRoot()) {
|
|
113
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
114
|
+
const dir = join(getStateDir(root), "locks");
|
|
115
|
+
ensureDir(dir);
|
|
116
|
+
return dir;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isPidAlive(pid) {
|
|
120
|
+
if (!pid || typeof pid !== "number") return false;
|
|
121
|
+
try {
|
|
122
|
+
process.kill(pid, 0);
|
|
123
|
+
return true;
|
|
124
|
+
} catch (_) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function acquireLock(agentName, taskId, files = [], rootOrOpts = resolveRoot()) {
|
|
130
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
131
|
+
const lockDir = getLockDir(root);
|
|
132
|
+
const lockFile = join(lockDir, `${taskId}.json`);
|
|
133
|
+
|
|
134
|
+
// Stale Lock Reaper Check
|
|
135
|
+
if (existsSync(lockFile)) {
|
|
136
|
+
try {
|
|
137
|
+
const existing = JSON.parse(readFileSync(lockFile, "utf-8"));
|
|
138
|
+
const isAlive = isPidAlive(existing.pid);
|
|
139
|
+
const isExpired = existing.acquiredAt && Date.now() - new Date(existing.acquiredAt).getTime() > 7200000;
|
|
140
|
+
|
|
141
|
+
if (!isAlive || isExpired) {
|
|
142
|
+
// Dead or expired holder; safely reap lock
|
|
143
|
+
try { unlinkSync(lockFile); } catch (_) {}
|
|
144
|
+
} else {
|
|
145
|
+
return { ok: false, holder: existing.agent, taskId, pid: existing.pid };
|
|
146
|
+
}
|
|
147
|
+
} catch (_) {
|
|
148
|
+
try { unlinkSync(lockFile); } catch (_) {}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const payload = {
|
|
153
|
+
agent: agentName,
|
|
154
|
+
taskId,
|
|
155
|
+
files,
|
|
156
|
+
pid: process.pid,
|
|
157
|
+
acquiredAt: new Date().toISOString(),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Atomic file creation via 'wx' flag (Fixes TOCTOU Race Condition)
|
|
161
|
+
try {
|
|
162
|
+
const fd = openSync(lockFile, "wx");
|
|
163
|
+
writeSync(fd, JSON.stringify(payload, null, 2), "utf-8");
|
|
164
|
+
closeSync(fd);
|
|
165
|
+
return { ok: true, lockFile };
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err.code === "EEXIST") {
|
|
168
|
+
try {
|
|
169
|
+
const existing = JSON.parse(readFileSync(lockFile, "utf-8"));
|
|
170
|
+
return { ok: false, holder: existing.agent, taskId };
|
|
171
|
+
} catch (_) {
|
|
172
|
+
return { ok: false, holder: "unknown", taskId };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function releaseLock(taskId, rootOrOpts = resolveRoot()) {
|
|
180
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
181
|
+
const lockDir = getLockDir(root);
|
|
182
|
+
const lockFile = join(lockDir, `${taskId}.json`);
|
|
183
|
+
|
|
184
|
+
if (existsSync(lockFile)) {
|
|
185
|
+
try {
|
|
186
|
+
unlinkSync(lockFile); // CRITICAL FIX: Fixed ESM unlinkSync (removed legacy require("node:fs"))
|
|
187
|
+
return true;
|
|
188
|
+
} catch (_) {}
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function lockStatus(rootOrOpts = resolveRoot()) {
|
|
194
|
+
const root = typeof rootOrOpts === "string" ? rootOrOpts : resolveRoot();
|
|
195
|
+
const lockDir = getLockDir(root);
|
|
196
|
+
const locks = [];
|
|
197
|
+
try {
|
|
198
|
+
const files = readdirSync(lockDir);
|
|
199
|
+
for (const file of files) {
|
|
200
|
+
if (file.endsWith(".json")) {
|
|
201
|
+
try {
|
|
202
|
+
const content = readFileSync(join(lockDir, file), "utf-8");
|
|
203
|
+
locks.push(Object.freeze(JSON.parse(content)));
|
|
204
|
+
} catch (_) {}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch (_) {}
|
|
208
|
+
return locks;
|
|
209
|
+
}
|
package/src/webhook.mjs
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Verify GitHub webhook HMAC SHA-256 signature against request payload.
|
|
6
|
+
* @param {Buffer|string} payload - Raw request body
|
|
7
|
+
* @param {string} signatureHeader - Value of X-Hub-Signature-256 header
|
|
8
|
+
* @param {string} secret - Configured webhook secret
|
|
9
|
+
* @returns {boolean} True if signature matches
|
|
10
|
+
*/
|
|
11
|
+
export function verifySignature(payload, signatureHeader, secret) {
|
|
12
|
+
if (!secret) return true; // If no secret is configured, skip verification (development mode)
|
|
13
|
+
if (!signatureHeader || !signatureHeader.startsWith("sha256=")) return false;
|
|
14
|
+
|
|
15
|
+
const hmac = createHmac("sha256", secret);
|
|
16
|
+
const bodyBuf = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf-8");
|
|
17
|
+
const digest = `sha256=${hmac.update(bodyBuf).digest("hex")}`;
|
|
18
|
+
|
|
19
|
+
const sigBuf = Buffer.from(signatureHeader, "utf-8");
|
|
20
|
+
const digestBuf = Buffer.from(digest, "utf-8");
|
|
21
|
+
|
|
22
|
+
if (sigBuf.length !== digestBuf.length) return false;
|
|
23
|
+
return timingSafeEqual(sigBuf, digestBuf);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse incoming webhook body buffer into a JS object.
|
|
28
|
+
* @param {Buffer} bodyBuf
|
|
29
|
+
* @param {string} contentType
|
|
30
|
+
* @returns {object} Parsed JSON payload
|
|
31
|
+
*/
|
|
32
|
+
export function parseWebhookPayload(bodyBuf, contentType = "application/json") {
|
|
33
|
+
const str = bodyBuf.toString("utf-8");
|
|
34
|
+
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
35
|
+
const params = new URLSearchParams(str);
|
|
36
|
+
const payloadStr = params.get("payload");
|
|
37
|
+
return payloadStr ? JSON.parse(payloadStr) : {};
|
|
38
|
+
}
|
|
39
|
+
return str ? JSON.parse(str) : {};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Route and handle recognized GitHub event types.
|
|
44
|
+
* @param {string} eventType - Value of X-GitHub-Event header
|
|
45
|
+
* @param {object} payload - Parsed payload body
|
|
46
|
+
* @param {object} [handlers] - Custom action callbacks
|
|
47
|
+
* @returns {object} Response summary { handled: boolean, action: string, details: object }
|
|
48
|
+
*/
|
|
49
|
+
export function routeWebhookEvent(eventType, payload, handlers = {}) {
|
|
50
|
+
if (eventType === "ping") {
|
|
51
|
+
return { handled: true, action: "ping", details: { zen: payload.zen, hookId: payload.hook_id } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (eventType === "pull_request") {
|
|
55
|
+
const action = payload.action;
|
|
56
|
+
const prNumber = payload.number || payload.pull_request?.number;
|
|
57
|
+
const repo = payload.repository?.full_name;
|
|
58
|
+
const branch = payload.pull_request?.head?.ref;
|
|
59
|
+
const merged = payload.pull_request?.merged || false;
|
|
60
|
+
|
|
61
|
+
const details = { action, prNumber, repo, branch, merged };
|
|
62
|
+
if (typeof handlers.onPullRequest === "function") {
|
|
63
|
+
handlers.onPullRequest(details, payload);
|
|
64
|
+
}
|
|
65
|
+
return { handled: true, action: `pull_request:${action}`, details };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (eventType === "workflow_run") {
|
|
69
|
+
const action = payload.action;
|
|
70
|
+
const name = payload.workflow?.name || payload.workflow_run?.name;
|
|
71
|
+
const conclusion = payload.workflow_run?.conclusion;
|
|
72
|
+
const repo = payload.repository?.full_name;
|
|
73
|
+
|
|
74
|
+
const details = { action, name, conclusion, repo };
|
|
75
|
+
if (typeof handlers.onWorkflowRun === "function") {
|
|
76
|
+
handlers.onWorkflowRun(details, payload);
|
|
77
|
+
}
|
|
78
|
+
return { handled: true, action: `workflow_run:${action}`, details };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { handled: false, action: eventType, details: {} };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Create zero-dependency HTTP Webhook Server.
|
|
86
|
+
* @param {object} config
|
|
87
|
+
* @param {number} [config.port=8787]
|
|
88
|
+
* @param {string} [config.secret]
|
|
89
|
+
* @param {object} [config.handlers]
|
|
90
|
+
* @param {function} [config.log]
|
|
91
|
+
* @returns {import("node:http").Server}
|
|
92
|
+
*/
|
|
93
|
+
export function createWebhookServer({ _port = 8787, secret = process.env.JULES_WEBHOOK_SECRET, handlers = {}, log = console.log } = {}) {
|
|
94
|
+
const server = createServer((req, res) => {
|
|
95
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
96
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
97
|
+
res.end(JSON.stringify({ status: "ok", timestamp: new Date().toISOString() }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (req.method !== "POST") {
|
|
102
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
103
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const chunks = [];
|
|
108
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
109
|
+
req.on("end", () => {
|
|
110
|
+
const bodyBuf = Buffer.concat(chunks);
|
|
111
|
+
const sigHeader = req.headers["x-hub-signature-256"];
|
|
112
|
+
const eventType = req.headers["x-github-event"];
|
|
113
|
+
const contentType = req.headers["content-type"] || "application/json";
|
|
114
|
+
|
|
115
|
+
if (secret && !verifySignature(bodyBuf, sigHeader, secret)) {
|
|
116
|
+
log(`[Webhook] Invalid signature received from ${req.socket.remoteAddress}`);
|
|
117
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
118
|
+
res.end(JSON.stringify({ error: "Invalid signature" }));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const payload = parseWebhookPayload(bodyBuf, contentType);
|
|
124
|
+
const result = routeWebhookEvent(eventType, payload, handlers);
|
|
125
|
+
|
|
126
|
+
log(`[Webhook] Event: ${eventType} (${result.action}) handled=${result.handled}`);
|
|
127
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
128
|
+
res.end(JSON.stringify({ received: true, result }));
|
|
129
|
+
} catch (err) {
|
|
130
|
+
log(`[Webhook] Error parsing payload: ${err.message}`);
|
|
131
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
132
|
+
res.end(JSON.stringify({ error: "Malformed payload" }));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return server;
|
|
138
|
+
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
name: Bug Report
|
|
2
|
-
description: Create a report to help us improve Jules Orchestrator Kit
|
|
3
|
-
title: "[BUG] "
|
|
4
|
-
labels: ["bug"]
|
|
5
|
-
assignees: []
|
|
6
|
-
body:
|
|
7
|
-
- type: markdown
|
|
8
|
-
attributes:
|
|
9
|
-
value: |
|
|
10
|
-
Thanks for taking the time to fill out this bug report!
|
|
11
|
-
- type: input
|
|
12
|
-
id: version
|
|
13
|
-
attributes:
|
|
14
|
-
label: "Node.js Version"
|
|
15
|
-
description: "What version of Node.js are you using? (e.g. v20.11.0)"
|
|
16
|
-
placeholder: "v20.x"
|
|
17
|
-
validations:
|
|
18
|
-
required: true
|
|
19
|
-
- type: dropdown
|
|
20
|
-
id: package_manager
|
|
21
|
-
attributes:
|
|
22
|
-
label: "Package Manager"
|
|
23
|
-
description: "Which package manager are you using?"
|
|
24
|
-
options:
|
|
25
|
-
- npm
|
|
26
|
-
- pnpm
|
|
27
|
-
- yarn
|
|
28
|
-
- bun
|
|
29
|
-
validations:
|
|
30
|
-
required: true
|
|
31
|
-
- type: dropdown
|
|
32
|
-
id: execution_mode
|
|
33
|
-
attributes:
|
|
34
|
-
label: "Execution Mode"
|
|
35
|
-
description: "How are you running Jules?"
|
|
36
|
-
options:
|
|
37
|
-
- API Mode (JULES_API_KEY set)
|
|
38
|
-
- CLI Mode (Local jules binary)
|
|
39
|
-
validations:
|
|
40
|
-
required: true
|
|
41
|
-
- type: textarea
|
|
42
|
-
id: description
|
|
43
|
-
attributes:
|
|
44
|
-
label: "Bug Description"
|
|
45
|
-
description: "A clear and concise description of what the bug is."
|
|
46
|
-
placeholder: "When I run npm run jules:queue..."
|
|
47
|
-
validations:
|
|
48
|
-
required: true
|
|
49
|
-
- type: textarea
|
|
50
|
-
id: reproduction
|
|
51
|
-
attributes:
|
|
52
|
-
label: "Steps to Reproduce"
|
|
53
|
-
description: "Steps to reproduce the behavior."
|
|
54
|
-
placeholder: "1. Go to...\n2. Click on...\n3. See error"
|
|
55
|
-
validations:
|
|
56
|
-
required: true
|
|
57
|
-
- type: textarea
|
|
58
|
-
id: expected
|
|
59
|
-
attributes:
|
|
60
|
-
label: "Expected Behavior"
|
|
61
|
-
description: "A clear and concise description of what you expected to happen."
|
|
62
|
-
validations:
|
|
63
|
-
required: true
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
name: Feature Request
|
|
2
|
-
description: Suggest an idea for Jules Orchestrator Kit
|
|
3
|
-
title: "[FEATURE] "
|
|
4
|
-
labels: ["enhancement"]
|
|
5
|
-
assignees: []
|
|
6
|
-
body:
|
|
7
|
-
- type: markdown
|
|
8
|
-
attributes:
|
|
9
|
-
value: |
|
|
10
|
-
Thanks for suggesting a new feature!
|
|
11
|
-
- type: textarea
|
|
12
|
-
id: description
|
|
13
|
-
attributes:
|
|
14
|
-
label: "Is your feature request related to a problem? Please describe."
|
|
15
|
-
description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
|
|
16
|
-
placeholder: "..."
|
|
17
|
-
validations:
|
|
18
|
-
required: true
|
|
19
|
-
- type: textarea
|
|
20
|
-
id: solution
|
|
21
|
-
attributes:
|
|
22
|
-
label: "Describe the solution you'd like"
|
|
23
|
-
description: "A clear and concise description of what you want to happen."
|
|
24
|
-
placeholder: "..."
|
|
25
|
-
validations:
|
|
26
|
-
required: true
|
|
27
|
-
- type: textarea
|
|
28
|
-
id: alternatives
|
|
29
|
-
attributes:
|
|
30
|
-
label: "Describe alternatives you've considered"
|
|
31
|
-
description: "A clear and concise description of any alternative solutions or features you've considered."
|
|
32
|
-
placeholder: "..."
|
|
33
|
-
validations:
|
|
34
|
-
required: false
|
|
Binary file
|