jules-orchestrator-kit 0.22.5 ā 0.22.7
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/README.md +10 -5
- package/bin/agentctl.mjs +6 -3
- package/index.mjs +1 -0
- package/package.json +1 -1
- package/src/engine.mjs +13 -4
- package/src/git.mjs +25 -1
- package/src/journal.mjs +230 -0
- package/src/mcp.mjs +5 -1
- package/src/prompt-guard.mjs +6 -2
- package/src/state.mjs +16 -7
package/README.md
CHANGED
|
@@ -68,8 +68,11 @@ Whether you are dispatching your first automated coding task or managing high-th
|
|
|
68
68
|
|
|
69
69
|
### Engine System Highlights
|
|
70
70
|
|
|
71
|
-
- **
|
|
72
|
-
- **
|
|
71
|
+
- **Native Task DAG Executor (`src/dag-engine.mjs`)**: Zero-dependency `DagExecutor` with Kahn's topological sort algorithm, SHA-256 interface fingerprinting post-task execution, and pre-execution cycle detection (`DagCycleError`).
|
|
72
|
+
- **Intent Journaling & Zombie Worktree Reaper (`src/journal.mjs`)**: Automatic boot-time scan (`reapOrphanedIntents`) in `agentctl` and MCP server that tracks git operations in `.agent/state/journal.jsonl` and prunes orphaned worktrees left by crashed/recycled processes.
|
|
73
|
+
- **Hermetic Network Egress Guard (`src/preload-net-guard.mjs`)**: Intercepts and blocks unmocked outbound HTTP/HTTPS egress during test execution (`NODE_OPTIONS="--import ./src/preload-net-guard.mjs"`), enforcing hermetic testing while allowing local loopback (`localhost`, `127.0.0.1`).
|
|
74
|
+
- **Linearizable VFS Mutex (`src/state.mjs`)**: Kernel-level directory mutex (`withVfsMutex`) guaranteeing serial linearizability for SHA-256 hash-chained session ledgers with atomic budget reservation (`reserveBudgetAtomic`).
|
|
75
|
+
- **PID Recycling & Stale Lock Protection (`src/state.mjs`)**: Linux `/proc/<pid>/stat` launch-time validation and random UUID nonces prevent false-positive lock reaps from recycled OS process IDs.
|
|
73
76
|
- **Memory-Bounded Content-Length MCP Streaming (`src/mcp.mjs`)**: Native MCP server over stdio streams using `McpFrameDecoder` with a 4 MB memory safety ceiling and panic boundaries to prevent stdout stack trace leaks.
|
|
74
77
|
- **Process Group Isolation (`src/process-group.mjs`)**: `ProcessGroupManager` creates isolated process groups (`detached: true`) and catches `SIGINT`/`SIGTERM`/`exit` signals to execute `process.kill(-pgid)`, guaranteeing zero zombie processes.
|
|
75
78
|
- **TOCTOU & Symlink Defense (`src/security.mjs`)**: `safeAtomicWrite()` uses `O_CREAT | O_EXCL | O_WRONLY` temp files with `fsyncSync` + `renameSync` and `lstatSync`/`realpathSync` symlink checks.
|
|
@@ -84,12 +87,14 @@ Whether you are dispatching your first automated coding task or managing high-th
|
|
|
84
87
|
|
|
85
88
|

|
|
86
89
|
|
|
87
|
-
### The 4-Phase Safety Audit (`agentctl gate`)
|
|
90
|
+
### The 4-Phase Safety Audit & Security Boundary (`agentctl gate`)
|
|
88
91
|
|
|
89
92
|
1. **Scope Fencing (`forbidden_paths`)**: Ensures agents cannot modify protected files (`package.json`, `.github/`, deployment keys) without explicit overrides.
|
|
90
93
|
2. **Diff Payload Governor**: Rejects oversized diffs (> 75 KB) to prevent truncation and hidden payload injections.
|
|
91
94
|
3. **Secret Entropy Scanner**: Scans diffs for high-confidence secrets (AWS keys, Stripe keys, GitHub tokens, SSH private keys) using Shannon Entropy analysis (> 3.6 bits).
|
|
92
|
-
4. **Trusted Verification Suite**: Executes auto-detected unit tests and linters (`npm test`) to guarantee zero regressions before merging.
|
|
95
|
+
4. **Trusted Verification Suite**: Executes auto-detected unit tests and linters (`npm test`) inside a hermetic network sandbox to guarantee zero regressions before merging.
|
|
96
|
+
5. **Prompt Guard Boundary (`src/prompt-guard.mjs`)**: `sanitizeUntrustedData` strips bidi control characters, ANSI escape sequences, zero-width unicode, and neutralizes prompt injection tags (`<|im_start|>`, `[INST]`).
|
|
97
|
+
6. **MCP Stream Isolation (`src/mcp.mjs`)**: Seals `process.stdout.write` framing stream to prevent log output from corrupting JSON-RPC stdio frames.
|
|
93
98
|
|
|
94
99
|
> [!NOTE]
|
|
95
100
|
> All security rules are fetched strictly from `origin/main` (never untrusted PR branches) to prevent prompt-injection attacks from altering security rules.
|
|
@@ -178,7 +183,7 @@ steps:
|
|
|
178
183
|
| **`mcp`** | `agentctl mcp` | Starts stdio Model Context Protocol (MCP) JSON-RPC 2.0 server |
|
|
179
184
|
| **`doctor`** | `agentctl doctor` | Verifies stack configuration, environment keys, and daily token budget |
|
|
180
185
|
| **`scan`** | `agentctl scan` | Scans codebase for `TODO` and `FIXME` comments and generates task queue |
|
|
181
|
-
| **`
|
|
186
|
+
| **`clean`** | `agentctl clean` | Audits and cleans up stale git worktrees, orphaned intents, locks, and temporary state files |
|
|
182
187
|
|
|
183
188
|
</details>
|
|
184
189
|
|
package/bin/agentctl.mjs
CHANGED
|
@@ -7,13 +7,14 @@ import { loadConfig, resolveRoot, detectStack } from "../src/config.mjs";
|
|
|
7
7
|
import { gate, dispatch, run, isTaskFile } from "../src/engine.mjs";
|
|
8
8
|
import { acquireLock, releaseLock, lockStatus, checkDailyBudget, getQueueDir, ensureDir } from "../src/state.mjs";
|
|
9
9
|
import { worktreePrune } from "../src/git.mjs";
|
|
10
|
+
import { reapOrphanedIntents, reapStaleMutexDirs } from "../src/journal.mjs";
|
|
10
11
|
|
|
11
12
|
const args = process.argv.slice(2);
|
|
12
13
|
const command = args[0];
|
|
13
14
|
|
|
14
15
|
function printHelp() {
|
|
15
16
|
console.log(`
|
|
16
|
-
š agentctl v0.22.
|
|
17
|
+
š agentctl v0.22.7 ā Universal Agent Orchestrator & Safety Gatekeeper
|
|
17
18
|
|
|
18
19
|
Usage: agentctl <command> [options]
|
|
19
20
|
|
|
@@ -43,11 +44,13 @@ async function main() {
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
46
|
-
console.log("agentctl v0.22.
|
|
47
|
+
console.log("agentctl v0.22.7");
|
|
47
48
|
process.exit(0);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const root = resolveRoot();
|
|
52
|
+
reapOrphanedIntents(root);
|
|
53
|
+
reapStaleMutexDirs(root);
|
|
51
54
|
const config = loadConfig(root);
|
|
52
55
|
|
|
53
56
|
switch (command) {
|
|
@@ -223,7 +226,7 @@ async function main() {
|
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
case "doctor": {
|
|
226
|
-
console.log(`\nš agentctl System Diagnostics (v0.22.
|
|
229
|
+
console.log(`\nš agentctl System Diagnostics (v0.22.7)`);
|
|
227
230
|
console.log(`--------------------------------------------------`);
|
|
228
231
|
console.log(` Project Root : ${root}`);
|
|
229
232
|
console.log(` Config File : ${config._file || "None (Using defaults)"}`);
|
package/index.mjs
CHANGED
|
@@ -50,6 +50,7 @@ export { checkAssetIntegrity } from "./src/asset_integrity.mjs";
|
|
|
50
50
|
export { classifyRiskTier, RISK_TIERS } from "./src/risk.mjs";
|
|
51
51
|
export { checkRulesBudget } from "./src/rules_budget.mjs";
|
|
52
52
|
export { DagExecutor, DagCycleError } from "./src/dag-engine.mjs";
|
|
53
|
+
export { journalIntent, journalDone, reapOrphanedIntents, reapStaleMutexDirs } from "./src/journal.mjs";
|
|
53
54
|
|
|
54
55
|
// Legacy SDK shims for backward compatibility
|
|
55
56
|
export { resolveProjectCommands, resolveWorkspaceExecutionBoundary } from "./scripts/command-resolver.mjs";
|
package/package.json
CHANGED
package/src/engine.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { checkScope, scanDiff, redactSecrets } from "./security.mjs";
|
|
|
3
3
|
import { changedFiles, diffBytes, diffText, showFromOrigin, runCmd } from "./git.mjs";
|
|
4
4
|
import { createProvider } from "./provider.mjs";
|
|
5
5
|
import { withBudget, appendLedger, getQueueDir, ensureDir } from "./state.mjs";
|
|
6
|
+
import { sanitizeUntrustedData, buildAgentEnvelope } from "./prompt-guard.mjs";
|
|
6
7
|
import { readdirSync, readFileSync, renameSync, existsSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
8
9
|
import { createHash } from "node:crypto";
|
|
@@ -164,8 +165,9 @@ export async function gate(opts = {}) {
|
|
|
164
165
|
let buildResult = { ok: true, status: 0 };
|
|
165
166
|
|
|
166
167
|
const existingNodeOptions = process.env.NODE_OPTIONS || "";
|
|
167
|
-
const
|
|
168
|
-
const
|
|
168
|
+
const netGuardUrl = new URL("./preload-net-guard.mjs", import.meta.url).href;
|
|
169
|
+
const netGuardFlag = `--import ${netGuardUrl}`;
|
|
170
|
+
const guardNodeOptions = existingNodeOptions && !existingNodeOptions.includes(netGuardFlag) && !existingNodeOptions.includes("preload-net-guard.mjs")
|
|
169
171
|
? `${existingNodeOptions} ${netGuardFlag}`
|
|
170
172
|
: existingNodeOptions ? existingNodeOptions : netGuardFlag;
|
|
171
173
|
|
|
@@ -354,8 +356,15 @@ export async function dispatch(task, opts = {}) {
|
|
|
354
356
|
}
|
|
355
357
|
|
|
356
358
|
// Redact secrets in prompt before dispatching
|
|
357
|
-
const cleanPrompt = redactSecrets(task.prompt);
|
|
358
|
-
|
|
359
|
+
const cleanPrompt = redactSecrets(task.prompt || "");
|
|
360
|
+
|
|
361
|
+
// Wire Prompt Guard & Envelope to wrap raw task arguments
|
|
362
|
+
const untrustedData = task.untrustedData || [cleanPrompt];
|
|
363
|
+
const systemPolicy = task.systemPolicy || config.systemPolicy || "";
|
|
364
|
+
const taskInstructions = task.taskInstructions || task.title || "Autonomous Task Execution";
|
|
365
|
+
const envelopedPrompt = buildAgentEnvelope(systemPolicy, taskInstructions, untrustedData);
|
|
366
|
+
|
|
367
|
+
const cleanTask = { ...task, prompt: envelopedPrompt };
|
|
359
368
|
|
|
360
369
|
return withBudget(
|
|
361
370
|
() => provider.dispatch(cleanTask, { root, dryRun: opts.dryRun }),
|
package/src/git.mjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { sep } from "node:path";
|
|
3
|
+
import { journalIntent, journalDone } from "./journal.mjs";
|
|
4
|
+
|
|
5
|
+
export const NET_GUARD_PRELOAD_URL = new URL("./preload-net-guard.mjs", import.meta.url).href;
|
|
6
|
+
export const NET_GUARD_FLAG = `--import ${NET_GUARD_PRELOAD_URL}`;
|
|
3
7
|
|
|
4
8
|
export class GateError extends Error {
|
|
5
9
|
constructor(message, opts = {}) {
|
|
@@ -174,8 +178,28 @@ export function showFromOrigin(root = process.cwd(), base = "main", filePath = "
|
|
|
174
178
|
}
|
|
175
179
|
}
|
|
176
180
|
|
|
181
|
+
export function createBranch(root = process.cwd(), branch = "") {
|
|
182
|
+
const opId = journalIntent(root, { type: "create_branch", branch, targetPath: "" });
|
|
183
|
+
try {
|
|
184
|
+
const res = git(["branch", branch], { cwd: root });
|
|
185
|
+
journalDone(root, opId);
|
|
186
|
+
return res;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
journalDone(root, opId);
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
177
193
|
export function worktreeAdd(root = process.cwd(), branch = "agent/task", targetDir = "") {
|
|
178
|
-
|
|
194
|
+
const opId = journalIntent(root, { type: "worktree_add", targetPath: targetDir, branch });
|
|
195
|
+
try {
|
|
196
|
+
const res = git(["worktree", "add", targetDir, "-b", branch], { cwd: root });
|
|
197
|
+
journalDone(root, opId);
|
|
198
|
+
return res;
|
|
199
|
+
} catch (err) {
|
|
200
|
+
journalDone(root, opId);
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
179
203
|
}
|
|
180
204
|
|
|
181
205
|
export function worktreeRemove(root = process.cwd(), targetDir = "") {
|
package/src/journal.mjs
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { openSync, writeSync, fsyncSync, closeSync, existsSync, readFileSync, unlinkSync, readdirSync, statSync, renameSync, rmdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { getStateDir, getProcessStartTime, isPidAlive, getLockDir } from "./state.mjs";
|
|
5
|
+
import { worktreeRemove, worktreePrune } from "./git.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Scans .agent/state/ for .mutex directories.
|
|
9
|
+
* If empty AND older than ttlMs, renames to a temporary .grave-<pid> path and rmdirSyncs it (atomic CAS pattern).
|
|
10
|
+
* @param {string} root - Project root directory
|
|
11
|
+
* @param {Object} [opts] - Options object
|
|
12
|
+
* @param {number} [opts.ttlMs=30000] - Time to live in milliseconds
|
|
13
|
+
* @returns {{ reapedCount: number, reaped: Array<string> }}
|
|
14
|
+
*/
|
|
15
|
+
export function reapStaleMutexDirs(root, { ttlMs = 30000 } = {}) {
|
|
16
|
+
const stateDir = getStateDir(root);
|
|
17
|
+
if (!existsSync(stateDir)) {
|
|
18
|
+
return { reapedCount: 0, reaped: [] };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const reaped = [];
|
|
22
|
+
try {
|
|
23
|
+
const entries = readdirSync(stateDir, { withFileTypes: true });
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
if (entry.isDirectory() && entry.name.endsWith(".mutex")) {
|
|
27
|
+
const mutexPath = join(stateDir, entry.name);
|
|
28
|
+
try {
|
|
29
|
+
const contents = readdirSync(mutexPath);
|
|
30
|
+
if (contents.length === 0) {
|
|
31
|
+
const stat = statSync(mutexPath);
|
|
32
|
+
const age = now - stat.mtimeMs;
|
|
33
|
+
if (age >= ttlMs) {
|
|
34
|
+
const gravePath = join(stateDir, `.grave-${process.pid}-${randomUUID()}`);
|
|
35
|
+
try {
|
|
36
|
+
renameSync(mutexPath, gravePath);
|
|
37
|
+
try {
|
|
38
|
+
rmdirSync(gravePath);
|
|
39
|
+
reaped.push(entry.name);
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
} catch (_) {}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
} catch (_) {}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} catch (_) {}
|
|
48
|
+
|
|
49
|
+
return { reapedCount: reaped.length, reaped };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Appends an intent record to .agent/state/journal.jsonl before a mutating operation.
|
|
54
|
+
* @param {string} root - Project root directory
|
|
55
|
+
* @param {Object|string} op - Operation payload or name
|
|
56
|
+
* @returns {string} opId - Unique operation identifier
|
|
57
|
+
*/
|
|
58
|
+
export function journalIntent(root, op) {
|
|
59
|
+
const stateDir = getStateDir(root);
|
|
60
|
+
const journalPath = join(stateDir, "journal.jsonl");
|
|
61
|
+
|
|
62
|
+
const opObj = typeof op === "string" ? { type: op } : op || {};
|
|
63
|
+
const opId = opObj.opId || `op-${randomUUID()}`;
|
|
64
|
+
const pid = process.pid;
|
|
65
|
+
const processStartTime = getProcessStartTime(pid);
|
|
66
|
+
|
|
67
|
+
const record = {
|
|
68
|
+
opId,
|
|
69
|
+
event: "intent",
|
|
70
|
+
type: opObj.type || opObj.operation || "unknown",
|
|
71
|
+
targetPath: opObj.targetPath || opObj.targetDir || opObj.path || "",
|
|
72
|
+
pid,
|
|
73
|
+
processStartTime,
|
|
74
|
+
timestamp: new Date().toISOString(),
|
|
75
|
+
...opObj,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const fd = openSync(journalPath, "a");
|
|
79
|
+
try {
|
|
80
|
+
writeSync(fd, JSON.stringify(record) + "\n", "utf-8");
|
|
81
|
+
fsyncSync(fd);
|
|
82
|
+
} finally {
|
|
83
|
+
closeSync(fd);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return opId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Appends a journal_done record to .agent/state/journal.jsonl upon operation completion.
|
|
91
|
+
* @param {string} root - Project root directory
|
|
92
|
+
* @param {string} opId - Operation identifier returned by journalIntent
|
|
93
|
+
* @returns {string|null} opId
|
|
94
|
+
*/
|
|
95
|
+
export function journalDone(root, opId) {
|
|
96
|
+
if (!opId) return null;
|
|
97
|
+
const stateDir = getStateDir(root);
|
|
98
|
+
const journalPath = join(stateDir, "journal.jsonl");
|
|
99
|
+
|
|
100
|
+
const record = {
|
|
101
|
+
opId,
|
|
102
|
+
event: "journal_done",
|
|
103
|
+
timestamp: new Date().toISOString(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const fd = openSync(journalPath, "a");
|
|
107
|
+
try {
|
|
108
|
+
writeSync(fd, JSON.stringify(record) + "\n", "utf-8");
|
|
109
|
+
fsyncSync(fd);
|
|
110
|
+
} finally {
|
|
111
|
+
closeSync(fd);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return opId;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scans .agent/state/journal.jsonl for uncompleted intents belonging to dead PIDs,
|
|
119
|
+
* prunes orphaned worktrees, removes stale lock files, and marks intents reaped.
|
|
120
|
+
* Idempotent: Subsequent calls result in 0 additional reaps.
|
|
121
|
+
* @param {string} root - Project root directory
|
|
122
|
+
* @returns {{ reapedCount: number, reaped: Array<Object> }}
|
|
123
|
+
*/
|
|
124
|
+
export function reapOrphanedIntents(root) {
|
|
125
|
+
const stateDir = getStateDir(root);
|
|
126
|
+
const journalPath = join(stateDir, "journal.jsonl");
|
|
127
|
+
|
|
128
|
+
if (!existsSync(journalPath)) {
|
|
129
|
+
return { reapedCount: 0, reaped: [] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let lines = [];
|
|
133
|
+
try {
|
|
134
|
+
const raw = readFileSync(journalPath, "utf-8");
|
|
135
|
+
lines = raw.split("\n").filter(Boolean);
|
|
136
|
+
} catch (_) {
|
|
137
|
+
return { reapedCount: 0, reaped: [] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const intents = new Map();
|
|
141
|
+
const finishedOpIds = new Set();
|
|
142
|
+
|
|
143
|
+
for (const line of lines) {
|
|
144
|
+
try {
|
|
145
|
+
const entry = JSON.parse(line);
|
|
146
|
+
if (!entry || !entry.opId) continue;
|
|
147
|
+
if (entry.event === "intent") {
|
|
148
|
+
intents.set(entry.opId, entry);
|
|
149
|
+
} else if (entry.event === "journal_done" || entry.event === "journal_reaped") {
|
|
150
|
+
finishedOpIds.add(entry.opId);
|
|
151
|
+
}
|
|
152
|
+
} catch (_) {
|
|
153
|
+
// Ignore corrupted or partial lines
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const reaped = [];
|
|
158
|
+
|
|
159
|
+
for (const [opId, intent] of intents.entries()) {
|
|
160
|
+
if (finishedOpIds.has(opId)) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const alive = isPidAlive(intent.pid, intent.processStartTime);
|
|
165
|
+
if (!alive) {
|
|
166
|
+
// Owner PID is dead or recycled -> reap orphaned intent
|
|
167
|
+
if (intent.targetPath) {
|
|
168
|
+
try {
|
|
169
|
+
worktreeRemove(root, intent.targetPath);
|
|
170
|
+
} catch (_) {}
|
|
171
|
+
try {
|
|
172
|
+
worktreePrune(root);
|
|
173
|
+
} catch (_) {}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Clean up stale lock files associated with this PID or targetPath
|
|
177
|
+
try {
|
|
178
|
+
const lockDir = getLockDir(root);
|
|
179
|
+
if (existsSync(lockDir)) {
|
|
180
|
+
const lockFiles = readdirSync(lockDir);
|
|
181
|
+
for (const file of lockFiles) {
|
|
182
|
+
if (file.endsWith(".json")) {
|
|
183
|
+
const lockPath = join(lockDir, file);
|
|
184
|
+
try {
|
|
185
|
+
const lockContent = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
186
|
+
const lockPid = lockContent.pid;
|
|
187
|
+
const lockStartTime = lockContent.processStartTime ?? lockContent.starttime ?? null;
|
|
188
|
+
|
|
189
|
+
// Do NOT unlink if the PID exists AND its start time matches a live process
|
|
190
|
+
if (lockPid && isPidAlive(lockPid, lockStartTime)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (
|
|
195
|
+
lockContent.pid === intent.pid ||
|
|
196
|
+
(intent.targetPath && Array.isArray(lockContent.files) && lockContent.files.includes(intent.targetPath))
|
|
197
|
+
) {
|
|
198
|
+
unlinkSync(lockPath);
|
|
199
|
+
}
|
|
200
|
+
} catch (_) {}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
} catch (_) {}
|
|
205
|
+
|
|
206
|
+
// Record journal_reaped to guarantee idempotency
|
|
207
|
+
const reapedRecord = {
|
|
208
|
+
opId,
|
|
209
|
+
event: "journal_reaped",
|
|
210
|
+
reapedAt: new Date().toISOString(),
|
|
211
|
+
pid: intent.pid,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const fd = openSync(journalPath, "a");
|
|
216
|
+
try {
|
|
217
|
+
writeSync(fd, JSON.stringify(reapedRecord) + "\n", "utf-8");
|
|
218
|
+
fsyncSync(fd);
|
|
219
|
+
} finally {
|
|
220
|
+
closeSync(fd);
|
|
221
|
+
}
|
|
222
|
+
} catch (_) {}
|
|
223
|
+
|
|
224
|
+
reaped.push(intent);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { reapedCount: reaped.length, reaped };
|
|
229
|
+
}
|
|
230
|
+
|
package/src/mcp.mjs
CHANGED
|
@@ -3,10 +3,11 @@ import { loadConfig, resolveRoot, detectStack } from "./config.mjs";
|
|
|
3
3
|
import { gate, dispatch } from "./engine.mjs";
|
|
4
4
|
import { classifyRiskTier } from "./risk.mjs";
|
|
5
5
|
import { checkDailyBudget, lockStatus } from "./state.mjs";
|
|
6
|
+
import { reapOrphanedIntents, reapStaleMutexDirs } from "./journal.mjs";
|
|
6
7
|
|
|
7
8
|
export const MCP_SERVER_INFO = {
|
|
8
9
|
name: "jules-orchestrator-kit",
|
|
9
|
-
version: "0.
|
|
10
|
+
version: "0.22.7",
|
|
10
11
|
};
|
|
11
12
|
|
|
12
13
|
export const MAX_MCP_FRAME_SIZE = 4 * 1024 * 1024; // 4 MB memory safety ceiling
|
|
@@ -352,6 +353,9 @@ export function writeMcpFrame(targetOutput, data) {
|
|
|
352
353
|
}
|
|
353
354
|
|
|
354
355
|
export function startMcpServer(input = process.stdin, output = process.stdout, opts = {}) {
|
|
356
|
+
const root = opts.root || resolveRoot();
|
|
357
|
+
reapOrphanedIntents(root);
|
|
358
|
+
reapStaleMutexDirs(root);
|
|
355
359
|
isolateMcpStdout(process.stdout, opts);
|
|
356
360
|
if (output && output !== process.stdout) {
|
|
357
361
|
isolateMcpStdout(output, opts);
|
package/src/prompt-guard.mjs
CHANGED
|
@@ -78,8 +78,12 @@ export function buildAgentEnvelope(systemPolicy = "", taskInstructions = "", unt
|
|
|
78
78
|
if (Array.isArray(untrustedDataArray)) {
|
|
79
79
|
for (const item of untrustedDataArray) {
|
|
80
80
|
if (typeof item === "string") {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
const trimmed = item.trim();
|
|
82
|
+
const match = /^<<<UNTRUSTED-DATA-BEGIN(?:\s+source="([^"]+)")?>\n?([\s\S]*?)\n?<<<UNTRUSTED-DATA-END>>>$/.exec(trimmed);
|
|
83
|
+
if (match) {
|
|
84
|
+
const src = match[1] || "untrusted";
|
|
85
|
+
const body = match[2];
|
|
86
|
+
sanitizedBlocks.push(sanitizeUntrustedData(body, src));
|
|
83
87
|
} else {
|
|
84
88
|
sanitizedBlocks.push(sanitizeUntrustedData(item, "untrusted"));
|
|
85
89
|
}
|
package/src/state.mjs
CHANGED
|
@@ -188,7 +188,15 @@ export function checkDailyBudget(arg1 = resolveRoot(), arg2 = 300) {
|
|
|
188
188
|
try {
|
|
189
189
|
const content = readFileSync(filePath, "utf-8");
|
|
190
190
|
const lines = content.split("\n").filter(Boolean);
|
|
191
|
-
|
|
191
|
+
let count = 0;
|
|
192
|
+
for (const line of lines) {
|
|
193
|
+
try {
|
|
194
|
+
const entry = JSON.parse(line);
|
|
195
|
+
if (entry && entry.event === "budget_reserved") {
|
|
196
|
+
count++;
|
|
197
|
+
}
|
|
198
|
+
} catch (_) {}
|
|
199
|
+
}
|
|
192
200
|
return {
|
|
193
201
|
ok: count < limit,
|
|
194
202
|
used: count,
|
|
@@ -224,13 +232,14 @@ export function reserveBudgetAtomic(stateDirOrRoot = resolveRoot(), limit = 300,
|
|
|
224
232
|
try {
|
|
225
233
|
const raw = readFileSync(filePath, "utf-8");
|
|
226
234
|
const lines = raw.split("\n").filter(Boolean);
|
|
227
|
-
|
|
228
|
-
for (let i = lines.length - 1; i >= 0; i--) {
|
|
235
|
+
for (const line of lines) {
|
|
229
236
|
try {
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
237
|
+
const entry = JSON.parse(line);
|
|
238
|
+
if (entry && entry.event === "budget_reserved") {
|
|
239
|
+
count++;
|
|
240
|
+
}
|
|
241
|
+
if (entry && entry.hash) {
|
|
242
|
+
prevHash = entry.hash;
|
|
234
243
|
}
|
|
235
244
|
} catch (_) {}
|
|
236
245
|
}
|