newmark-agent 0.6.7 → 0.6.8
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/cli-commands.js +1 -1
- package/dist/conversation-utility-host.bundle.cjs +2060 -1166
- package/dist/core/agent.js +2 -0
- package/dist/core/agentKernelRunner.js +4 -0
- package/dist/core/computerUseSession.js +1 -1
- package/dist/core/fileWriteObservation.d.ts +41 -0
- package/dist/core/fileWriteObservation.js +454 -0
- package/dist/core/toolPolicy.d.ts +1 -1
- package/dist/core/toolPolicy.js +1 -1
- package/dist/tools/computerUse.d.ts +2 -0
- package/dist/tools/computerUse.js +206 -19
- package/dist/tools/index.d.ts +3 -4
- package/dist/tools/index.js +144 -67
- package/dist/wsl-agent-host.bundle.cjs +2060 -1166
- package/package.json +5 -3
package/dist/core/agent.js
CHANGED
|
@@ -5429,6 +5429,8 @@ class Agent {
|
|
|
5429
5429
|
conversationId: this.activeConversationId || 'default',
|
|
5430
5430
|
actorId: this.runtimeActorId,
|
|
5431
5431
|
workspaceId: this.workspace.current?.id || '',
|
|
5432
|
+
runtimeKey: process.env.NEWMARK_RUNTIME_KEY || undefined,
|
|
5433
|
+
runId: this.currentWorkRunId() || undefined,
|
|
5432
5434
|
backend: process.env.NEWMARK_WSL_DISTRO ? 'wsl' : (process.platform === 'win32' ? 'windows' : process.platform),
|
|
5433
5435
|
signal,
|
|
5434
5436
|
}).then((content) => {
|
|
@@ -1964,6 +1964,8 @@ async function executeNewmarkTool(agent, name, args, inputSchema, signal, onSett
|
|
|
1964
1964
|
conversationId: agent.activeConversationId || 'default',
|
|
1965
1965
|
actorId: agent.runtimeActorId,
|
|
1966
1966
|
workspaceId: (0, terminalTakeover_1.terminalTakeoverWorkspaceId)(wsDir),
|
|
1967
|
+
runtimeKey: process.env.NEWMARK_RUNTIME_KEY || undefined,
|
|
1968
|
+
runId: agent.currentWorkRunId() || undefined,
|
|
1967
1969
|
backend: process.env.NEWMARK_WSL_DISTRO ? 'wsl' : (process.platform === 'win32' ? 'windows' : process.platform),
|
|
1968
1970
|
signal,
|
|
1969
1971
|
});
|
|
@@ -2016,6 +2018,8 @@ async function executeNewmarkTool(agent, name, args, inputSchema, signal, onSett
|
|
|
2016
2018
|
conversationId: agent.activeConversationId || 'default',
|
|
2017
2019
|
actorId: agent.runtimeActorId,
|
|
2018
2020
|
workspaceId: (0, terminalTakeover_1.terminalTakeoverWorkspaceId)(wsDir),
|
|
2021
|
+
runtimeKey: process.env.NEWMARK_RUNTIME_KEY || undefined,
|
|
2022
|
+
runId: agent.currentWorkRunId() || undefined,
|
|
2019
2023
|
backend: process.env.NEWMARK_WSL_DISTRO ? 'wsl' : (process.platform === 'win32' ? 'windows' : process.platform),
|
|
2020
2024
|
inspectBrowserImage: name === 'browser_use' ? (dataUrl, prompt) => agent.inspectBrowserImage(dataUrl, prompt, signal) : undefined,
|
|
2021
2025
|
allowEphemeralVisionImage: (name === 'screen_capture' || name === 'computer_use' || name === 'browser_use' || name === 'pdf_read' || name === 'ocr_read'),
|
|
@@ -21,7 +21,7 @@ class ComputerUseSessionRegistry {
|
|
|
21
21
|
if (normalizedAction === 'takeover_stop')
|
|
22
22
|
return null;
|
|
23
23
|
const enabled = this.enabledByRuntime.get(runtimeKey) !== false;
|
|
24
|
-
const readOnly = normalizedAction === 'observe' || normalizedAction === 'app_list' || normalizedAction === 'app_observe' || normalizedAction === 'wait';
|
|
24
|
+
const readOnly = normalizedAction === 'observe' || normalizedAction === 'app_list' || normalizedAction === 'app_observe' || normalizedAction === 'wait' || normalizedAction === 'wait_for';
|
|
25
25
|
if (!enabled && !readOnly && !dryRun && normalizedAction !== 'takeover_start') {
|
|
26
26
|
return JSON.stringify({
|
|
27
27
|
ok: false,
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface FileReadRange {
|
|
2
|
+
startLine?: number;
|
|
3
|
+
endLine?: number;
|
|
4
|
+
}
|
|
5
|
+
export type FileObservationRequirement = {
|
|
6
|
+
kind: 'full';
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'edit';
|
|
9
|
+
oldText: string;
|
|
10
|
+
};
|
|
11
|
+
export interface FileMutationContext {
|
|
12
|
+
targetPath: string;
|
|
13
|
+
canonicalPath: string;
|
|
14
|
+
exists: boolean;
|
|
15
|
+
bytes?: Buffer;
|
|
16
|
+
text?: string;
|
|
17
|
+
assertBeforeCommit(): void;
|
|
18
|
+
}
|
|
19
|
+
export interface FileMutationOutcome<T> {
|
|
20
|
+
value: T;
|
|
21
|
+
committed: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type FileMutationResult<T> = {
|
|
24
|
+
ok: true;
|
|
25
|
+
value: T;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
error: string;
|
|
29
|
+
};
|
|
30
|
+
export declare class FileWriteObservationGate {
|
|
31
|
+
private readonly receipts;
|
|
32
|
+
read(targetPath: string, scope: string, range?: FileReadRange): Promise<string>;
|
|
33
|
+
mutate<T>(targetPath: string, scope: string, requirement: FileObservationRequirement, apply: (context: FileMutationContext) => Promise<FileMutationOutcome<T>> | FileMutationOutcome<T>): Promise<FileMutationResult<T>>;
|
|
34
|
+
private inspect;
|
|
35
|
+
private inspectMissing;
|
|
36
|
+
private assertBeforeCommit;
|
|
37
|
+
private receiptKey;
|
|
38
|
+
private pruneReceipts;
|
|
39
|
+
private withLock;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=fileWriteObservation.d.ts.map
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FileWriteObservationGate = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const READ_CHAR_LIMIT = 20_000;
|
|
41
|
+
const RECEIPT_TTL_MS = 5 * 60 * 1000;
|
|
42
|
+
const MAX_INLINE_RESULT_CHARS = 24_000;
|
|
43
|
+
const MAX_RECEIPTS = 256;
|
|
44
|
+
const MAX_READ_RANGES = 128;
|
|
45
|
+
const MAX_RECEIPT_CHARS = 150_000;
|
|
46
|
+
const processPathMutationLocks = new Map();
|
|
47
|
+
function errorCode(error) {
|
|
48
|
+
return error && typeof error === 'object' ? String(error.code || '') : '';
|
|
49
|
+
}
|
|
50
|
+
function sha256(value) {
|
|
51
|
+
return crypto.createHash('sha256').update(value).digest('hex');
|
|
52
|
+
}
|
|
53
|
+
function statIdentity(stat) {
|
|
54
|
+
return [
|
|
55
|
+
stat.dev,
|
|
56
|
+
stat.ino,
|
|
57
|
+
stat.mode,
|
|
58
|
+
stat.size,
|
|
59
|
+
stat.mtimeMs,
|
|
60
|
+
stat.ctimeMs,
|
|
61
|
+
].join(':');
|
|
62
|
+
}
|
|
63
|
+
function directoryIdentity(stat) {
|
|
64
|
+
return [stat.dev, stat.ino, stat.mode].join(':');
|
|
65
|
+
}
|
|
66
|
+
function lineStarts(text) {
|
|
67
|
+
const starts = [0];
|
|
68
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
69
|
+
if (text.charCodeAt(index) === 10)
|
|
70
|
+
starts.push(index + 1);
|
|
71
|
+
}
|
|
72
|
+
return starts;
|
|
73
|
+
}
|
|
74
|
+
function lineCount(text, starts = lineStarts(text)) {
|
|
75
|
+
if (text.length === 0)
|
|
76
|
+
return 0;
|
|
77
|
+
const newlineCount = starts.length - 1;
|
|
78
|
+
return text.endsWith('\n') ? newlineCount : newlineCount + 1;
|
|
79
|
+
}
|
|
80
|
+
function mergeLineRanges(ranges, totalLines) {
|
|
81
|
+
if (totalLines === 0)
|
|
82
|
+
return ranges.length > 0;
|
|
83
|
+
const sorted = [...ranges].sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
|
|
84
|
+
let nextLine = 1;
|
|
85
|
+
for (const range of sorted) {
|
|
86
|
+
if (range.endLine < nextLine)
|
|
87
|
+
continue;
|
|
88
|
+
if (range.startLine > nextLine)
|
|
89
|
+
return false;
|
|
90
|
+
nextLine = Math.max(nextLine, range.endLine + 1);
|
|
91
|
+
if (nextLine > totalLines)
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
return nextLine > totalLines;
|
|
95
|
+
}
|
|
96
|
+
function pathKey(value) {
|
|
97
|
+
const normalized = path.resolve(value);
|
|
98
|
+
const windowsMountedPath = /^\/mnt\/[a-z]\//i.test(normalized);
|
|
99
|
+
return process.platform === 'win32' || windowsMountedPath ? normalized.toLowerCase() : normalized;
|
|
100
|
+
}
|
|
101
|
+
class FileWriteObservationGate {
|
|
102
|
+
receipts = new Map();
|
|
103
|
+
async read(targetPath, scope, range = {}) {
|
|
104
|
+
const absolutePath = path.resolve(targetPath);
|
|
105
|
+
const receiptKey = this.receiptKey(scope, absolutePath);
|
|
106
|
+
this.pruneReceipts();
|
|
107
|
+
const initial = this.inspect(absolutePath);
|
|
108
|
+
const lockKey = pathKey(initial.canonicalPath || absolutePath);
|
|
109
|
+
return this.withLock(lockKey, () => {
|
|
110
|
+
const snapshot = this.inspect(absolutePath);
|
|
111
|
+
if (snapshot.kind === 'blocked') {
|
|
112
|
+
this.receipts.delete(receiptKey);
|
|
113
|
+
return '[read blocked] ' + snapshot.error;
|
|
114
|
+
}
|
|
115
|
+
if (pathKey(snapshot.canonicalPath) !== lockKey) {
|
|
116
|
+
this.receipts.delete(receiptKey);
|
|
117
|
+
return '[read blocked] The target path changed while it was being read. Read it again.';
|
|
118
|
+
}
|
|
119
|
+
if (snapshot.kind === 'missing') {
|
|
120
|
+
this.receipts.set(receiptKey, {
|
|
121
|
+
canonicalPath: snapshot.canonicalPath,
|
|
122
|
+
exists: false,
|
|
123
|
+
fingerprint: snapshot.fingerprint,
|
|
124
|
+
observedAt: Date.now(),
|
|
125
|
+
totalLines: 0,
|
|
126
|
+
full: true,
|
|
127
|
+
ranges: [],
|
|
128
|
+
});
|
|
129
|
+
return '[read] Target does not exist: ' + absolutePath
|
|
130
|
+
+ '\n[write gate] Exact-path absence observed. A write may create this target; the observation expires in five minutes.'
|
|
131
|
+
+ '\nNearest existing directory: ' + snapshot.parentAnchor.canonicalPath;
|
|
132
|
+
}
|
|
133
|
+
const startProvided = range.startLine !== undefined;
|
|
134
|
+
const endProvided = range.endLine !== undefined;
|
|
135
|
+
if (startProvided !== endProvided) {
|
|
136
|
+
return '[read blocked] Provide both start_line and end_line, or omit both.';
|
|
137
|
+
}
|
|
138
|
+
const explicitRange = startProvided && endProvided;
|
|
139
|
+
const text = snapshot.text;
|
|
140
|
+
const starts = lineStarts(text);
|
|
141
|
+
const totalLines = snapshot.totalLines;
|
|
142
|
+
let startLine = 1;
|
|
143
|
+
let endLine = totalLines;
|
|
144
|
+
let selected = text;
|
|
145
|
+
let fullRead = !explicitRange;
|
|
146
|
+
if (explicitRange) {
|
|
147
|
+
startLine = Number(range.startLine);
|
|
148
|
+
endLine = Number(range.endLine);
|
|
149
|
+
if (!Number.isInteger(startLine) || !Number.isInteger(endLine) || startLine < 1 || endLine < startLine) {
|
|
150
|
+
return '[read blocked] start_line/end_line must be positive integers with end_line >= start_line.';
|
|
151
|
+
}
|
|
152
|
+
if (totalLines === 0) {
|
|
153
|
+
if (startLine !== 1 || endLine !== 1)
|
|
154
|
+
return '[read blocked] The empty file only supports line range 1-1.';
|
|
155
|
+
startLine = 1;
|
|
156
|
+
endLine = 0;
|
|
157
|
+
selected = '';
|
|
158
|
+
fullRead = true;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
if (startLine > totalLines || endLine > totalLines) {
|
|
162
|
+
return '[read blocked] Requested line range is outside this file (1-' + totalLines + ').';
|
|
163
|
+
}
|
|
164
|
+
const startOffset = starts[startLine - 1];
|
|
165
|
+
const endOffset = endLine >= totalLines ? text.length : starts[endLine];
|
|
166
|
+
selected = text.slice(startOffset, endOffset);
|
|
167
|
+
fullRead = startLine === 1 && endLine === totalLines;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!explicitRange && text.length > READ_CHAR_LIMIT) {
|
|
171
|
+
const prior = this.receipts.get(receiptKey);
|
|
172
|
+
if (!prior || prior.fingerprint !== snapshot.fingerprint || Date.now() - prior.observedAt > RECEIPT_TTL_MS) {
|
|
173
|
+
this.receipts.delete(receiptKey);
|
|
174
|
+
}
|
|
175
|
+
return text.slice(0, READ_CHAR_LIMIT)
|
|
176
|
+
+ '\n...[truncated]'
|
|
177
|
+
+ '\n[write gate] No new full-read receipt was issued. Use read with start_line/end_line to inspect exact ranges; overwrite/delete requires complete line coverage.';
|
|
178
|
+
}
|
|
179
|
+
if (selected.length > READ_CHAR_LIMIT) {
|
|
180
|
+
return '[read blocked] Requested range exceeds the 20000-character read limit. Split it into smaller line ranges.';
|
|
181
|
+
}
|
|
182
|
+
if (selected.length + absolutePath.length + 1200 > MAX_INLINE_RESULT_CHARS) {
|
|
183
|
+
return '[read blocked] The returned path and line range would be truncated before reaching the model. Read a smaller range; no mutation receipt was issued.';
|
|
184
|
+
}
|
|
185
|
+
if (!snapshot.receiptEligible) {
|
|
186
|
+
this.receipts.delete(receiptKey);
|
|
187
|
+
const content = selected || (text.length === 0 ? '[empty file]' : '');
|
|
188
|
+
return content
|
|
189
|
+
+ '\n[write gate] No mutation receipt: ' + (snapshot.receiptReason || 'the target is not a stable plain-text file.');
|
|
190
|
+
}
|
|
191
|
+
const now = Date.now();
|
|
192
|
+
let receipt = this.receipts.get(receiptKey);
|
|
193
|
+
if (!receipt
|
|
194
|
+
|| receipt.fingerprint !== snapshot.fingerprint
|
|
195
|
+
|| receipt.canonicalPath !== snapshot.canonicalPath
|
|
196
|
+
|| now - receipt.observedAt > RECEIPT_TTL_MS) {
|
|
197
|
+
receipt = {
|
|
198
|
+
canonicalPath: snapshot.canonicalPath,
|
|
199
|
+
exists: true,
|
|
200
|
+
fingerprint: snapshot.fingerprint,
|
|
201
|
+
observedAt: now,
|
|
202
|
+
totalLines,
|
|
203
|
+
full: false,
|
|
204
|
+
ranges: [],
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const replacedRange = receipt.ranges.find(item => item.startLine === startLine && item.endLine === endLine);
|
|
208
|
+
const nextRangeCount = receipt.ranges.length + (replacedRange ? 0 : 1);
|
|
209
|
+
const currentReceiptChars = receipt.ranges.reduce((total, item) => total + item.text.length, 0);
|
|
210
|
+
const nextReceiptChars = currentReceiptChars - (replacedRange?.text.length || 0) + selected.length;
|
|
211
|
+
if (nextRangeCount > MAX_READ_RANGES || nextReceiptChars > MAX_RECEIPT_CHARS) {
|
|
212
|
+
return '[read blocked] The observation ledger reached its bounded range capacity. Read fewer, larger ranges; no new receipt range was added.';
|
|
213
|
+
}
|
|
214
|
+
receipt.observedAt = now;
|
|
215
|
+
receipt.ranges = receipt.ranges.filter(item => item.startLine !== startLine || item.endLine !== endLine);
|
|
216
|
+
receipt.ranges.push({ startLine, endLine, text: selected });
|
|
217
|
+
receipt.full = receipt.full || fullRead || mergeLineRanges(receipt.ranges, totalLines);
|
|
218
|
+
this.receipts.set(receiptKey, receipt);
|
|
219
|
+
this.pruneReceipts();
|
|
220
|
+
const content = selected || (text.length === 0 ? '[empty file]' : '');
|
|
221
|
+
const rangeLabel = explicitRange
|
|
222
|
+
? '[read] ' + absolutePath + ' (lines ' + startLine + '-' + endLine + ' of ' + totalLines + ')'
|
|
223
|
+
: '[read] ' + absolutePath + ' (complete text; ' + totalLines + ' lines)';
|
|
224
|
+
const grant = receipt.full
|
|
225
|
+
? '\n[write gate] Complete target observation recorded for this conversation, actor, workspace, and run. It is single-use and expires in five minutes.'
|
|
226
|
+
: '\n[write gate] Lines observed; edit may target text inside this range. Read every line before overwrite/delete.';
|
|
227
|
+
return rangeLabel + '\n--- BEGIN READ CONTENT ---\n' + content + '\n--- END READ CONTENT ---' + grant;
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
async mutate(targetPath, scope, requirement, apply) {
|
|
231
|
+
const absolutePath = path.resolve(targetPath);
|
|
232
|
+
const receiptKey = this.receiptKey(scope, absolutePath);
|
|
233
|
+
this.pruneReceipts();
|
|
234
|
+
const initial = this.inspect(absolutePath);
|
|
235
|
+
const lockKey = pathKey(initial.canonicalPath || absolutePath);
|
|
236
|
+
return this.withLock(lockKey, async () => {
|
|
237
|
+
const snapshot = this.inspect(absolutePath);
|
|
238
|
+
if (snapshot.kind === 'blocked') {
|
|
239
|
+
return { ok: false, error: '[write gate blocked] ' + snapshot.error };
|
|
240
|
+
}
|
|
241
|
+
if (pathKey(snapshot.canonicalPath) !== lockKey) {
|
|
242
|
+
return { ok: false, error: '[write gate blocked] The target path changed before mutation. Read the target again.' };
|
|
243
|
+
}
|
|
244
|
+
const receipt = this.receipts.get(receiptKey);
|
|
245
|
+
if (!receipt) {
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
error: '[write gate blocked] Read this exact target with the read tool before changing it. A read of another path, grep/glob output, or a write function internal read does not count.',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (Date.now() - receipt.observedAt > RECEIPT_TTL_MS) {
|
|
252
|
+
this.receipts.delete(receiptKey);
|
|
253
|
+
return { ok: false, error: '[write gate blocked] The read observation expired. Read the target again before changing it.' };
|
|
254
|
+
}
|
|
255
|
+
if (receipt.exists !== (snapshot.kind === 'file')
|
|
256
|
+
|| receipt.canonicalPath !== snapshot.canonicalPath
|
|
257
|
+
|| receipt.fingerprint !== snapshot.fingerprint) {
|
|
258
|
+
this.receipts.delete(receiptKey);
|
|
259
|
+
return { ok: false, error: '[write gate blocked] The target changed after it was read. Read the current target again before changing it.' };
|
|
260
|
+
}
|
|
261
|
+
if (requirement.kind === 'full' && snapshot.kind === 'file' && !receipt.full) {
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
error: '[write gate blocked] The complete file has not been read. Use read with line ranges that cover the whole file before overwrite/delete.',
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
if (requirement.kind === 'edit') {
|
|
268
|
+
const oldText = requirement.oldText;
|
|
269
|
+
if (!oldText || !receipt.ranges.some(item => item.text.includes(oldText))) {
|
|
270
|
+
return {
|
|
271
|
+
ok: false,
|
|
272
|
+
error: '[write gate blocked] Read the exact lines containing old_str before editing. Use read with a range that includes the full match.',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const context = {
|
|
277
|
+
targetPath: absolutePath,
|
|
278
|
+
canonicalPath: snapshot.canonicalPath,
|
|
279
|
+
exists: snapshot.kind === 'file',
|
|
280
|
+
...(snapshot.kind === 'file' ? { bytes: snapshot.bytes, text: snapshot.text } : {}),
|
|
281
|
+
assertBeforeCommit: () => this.assertBeforeCommit(absolutePath, snapshot),
|
|
282
|
+
};
|
|
283
|
+
const outcome = await apply(context);
|
|
284
|
+
if (outcome.committed)
|
|
285
|
+
this.receipts.delete(receiptKey);
|
|
286
|
+
return { ok: true, value: outcome.value };
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
inspect(targetPath) {
|
|
290
|
+
const absolutePath = path.resolve(targetPath);
|
|
291
|
+
let linkStat;
|
|
292
|
+
try {
|
|
293
|
+
linkStat = fs.lstatSync(absolutePath);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (errorCode(error) !== 'ENOENT') {
|
|
297
|
+
return { kind: 'blocked', targetPath: absolutePath, error: 'Unable to inspect target: ' + String(error) };
|
|
298
|
+
}
|
|
299
|
+
return this.inspectMissing(absolutePath);
|
|
300
|
+
}
|
|
301
|
+
let canonicalPath;
|
|
302
|
+
let followedStat;
|
|
303
|
+
try {
|
|
304
|
+
canonicalPath = fs.realpathSync.native(absolutePath);
|
|
305
|
+
followedStat = fs.statSync(absolutePath);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
return { kind: 'blocked', targetPath: absolutePath, error: 'Unable to resolve target: ' + String(error) };
|
|
309
|
+
}
|
|
310
|
+
if (!followedStat.isFile()) {
|
|
311
|
+
return {
|
|
312
|
+
kind: 'blocked',
|
|
313
|
+
targetPath: absolutePath,
|
|
314
|
+
canonicalPath,
|
|
315
|
+
error: 'The target is not a regular file: ' + absolutePath,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
let bytes;
|
|
319
|
+
let afterLinkStat;
|
|
320
|
+
let afterFollowedStat;
|
|
321
|
+
let afterCanonicalPath;
|
|
322
|
+
try {
|
|
323
|
+
bytes = fs.readFileSync(absolutePath);
|
|
324
|
+
afterLinkStat = fs.lstatSync(absolutePath);
|
|
325
|
+
afterFollowedStat = fs.statSync(absolutePath);
|
|
326
|
+
afterCanonicalPath = fs.realpathSync.native(absolutePath);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
return { kind: 'blocked', targetPath: absolutePath, canonicalPath, error: 'Unable to read target: ' + String(error) };
|
|
330
|
+
}
|
|
331
|
+
if (canonicalPath !== afterCanonicalPath
|
|
332
|
+
|| statIdentity(linkStat) !== statIdentity(afterLinkStat)
|
|
333
|
+
|| statIdentity(followedStat) !== statIdentity(afterFollowedStat)) {
|
|
334
|
+
return {
|
|
335
|
+
kind: 'blocked',
|
|
336
|
+
targetPath: absolutePath,
|
|
337
|
+
canonicalPath: afterCanonicalPath,
|
|
338
|
+
error: 'The target changed while it was being read. Read it again.',
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
const text = bytes.toString('utf8');
|
|
342
|
+
const validUtf8 = Buffer.compare(Buffer.from(text, 'utf8'), bytes) === 0;
|
|
343
|
+
const isSymbolicLink = linkStat.isSymbolicLink();
|
|
344
|
+
const receiptReason = isSymbolicLink
|
|
345
|
+
? 'the target is a symbolic link; use its resolved file path'
|
|
346
|
+
: (!validUtf8 ? 'the target is not valid UTF-8 text' : '');
|
|
347
|
+
const fingerprint = sha256(JSON.stringify({
|
|
348
|
+
canonicalPath,
|
|
349
|
+
linkIdentity: statIdentity(linkStat),
|
|
350
|
+
fileIdentity: statIdentity(followedStat),
|
|
351
|
+
contentHash: sha256(bytes),
|
|
352
|
+
}));
|
|
353
|
+
return {
|
|
354
|
+
kind: 'file',
|
|
355
|
+
targetPath: absolutePath,
|
|
356
|
+
canonicalPath,
|
|
357
|
+
fingerprint,
|
|
358
|
+
bytes,
|
|
359
|
+
text,
|
|
360
|
+
totalLines: lineCount(text),
|
|
361
|
+
receiptEligible: !isSymbolicLink && validUtf8,
|
|
362
|
+
...(receiptReason ? { receiptReason } : {}),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
inspectMissing(targetPath) {
|
|
366
|
+
const parentPath = path.dirname(targetPath);
|
|
367
|
+
let cursor = parentPath;
|
|
368
|
+
let anchor;
|
|
369
|
+
while (true) {
|
|
370
|
+
try {
|
|
371
|
+
const stat = fs.statSync(cursor);
|
|
372
|
+
if (!stat.isDirectory()) {
|
|
373
|
+
return { kind: 'blocked', targetPath, error: 'The parent path is not a directory: ' + cursor };
|
|
374
|
+
}
|
|
375
|
+
const canonicalPath = fs.realpathSync.native(cursor);
|
|
376
|
+
anchor = {
|
|
377
|
+
lexicalPath: path.resolve(cursor),
|
|
378
|
+
canonicalPath,
|
|
379
|
+
identity: directoryIdentity(stat),
|
|
380
|
+
};
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
if (errorCode(error) !== 'ENOENT' || path.dirname(cursor) === cursor) {
|
|
385
|
+
return { kind: 'blocked', targetPath, error: 'Unable to inspect the parent directory: ' + String(error) };
|
|
386
|
+
}
|
|
387
|
+
cursor = path.dirname(cursor);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
const canonicalTarget = path.resolve(anchor.canonicalPath, path.relative(anchor.lexicalPath, targetPath));
|
|
391
|
+
const fingerprint = sha256(JSON.stringify({
|
|
392
|
+
kind: 'missing',
|
|
393
|
+
canonicalTarget,
|
|
394
|
+
parentPath: anchor.canonicalPath,
|
|
395
|
+
parentIdentity: anchor.identity,
|
|
396
|
+
}));
|
|
397
|
+
return {
|
|
398
|
+
kind: 'missing',
|
|
399
|
+
targetPath,
|
|
400
|
+
canonicalPath: canonicalTarget,
|
|
401
|
+
fingerprint,
|
|
402
|
+
parentAnchor: anchor,
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
assertBeforeCommit(targetPath, observed) {
|
|
406
|
+
const current = this.inspect(targetPath);
|
|
407
|
+
if (observed.kind === 'file') {
|
|
408
|
+
if (current.kind !== 'file'
|
|
409
|
+
|| current.canonicalPath !== observed.canonicalPath
|
|
410
|
+
|| current.fingerprint !== observed.fingerprint) {
|
|
411
|
+
throw new Error('The target changed after it was read. Read it again before changing it.');
|
|
412
|
+
}
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (observed.kind !== 'missing' || current.kind === 'blocked' || current.kind === 'file'
|
|
416
|
+
|| current.canonicalPath !== observed.canonicalPath) {
|
|
417
|
+
throw new Error('The target path changed after it was read. Read it again before creating it.');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
receiptKey(scope, targetPath) {
|
|
421
|
+
return scope + '\u0000' + pathKey(targetPath);
|
|
422
|
+
}
|
|
423
|
+
pruneReceipts() {
|
|
424
|
+
const now = Date.now();
|
|
425
|
+
for (const [key, receipt] of this.receipts) {
|
|
426
|
+
if (now - receipt.observedAt > RECEIPT_TTL_MS)
|
|
427
|
+
this.receipts.delete(key);
|
|
428
|
+
}
|
|
429
|
+
while (this.receipts.size > MAX_RECEIPTS) {
|
|
430
|
+
const oldest = this.receipts.keys().next().value;
|
|
431
|
+
if (oldest === undefined)
|
|
432
|
+
break;
|
|
433
|
+
this.receipts.delete(oldest);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async withLock(key, action) {
|
|
437
|
+
const previous = processPathMutationLocks.get(key);
|
|
438
|
+
let release;
|
|
439
|
+
const current = new Promise(resolve => { release = resolve; });
|
|
440
|
+
processPathMutationLocks.set(key, current);
|
|
441
|
+
if (previous)
|
|
442
|
+
await previous;
|
|
443
|
+
try {
|
|
444
|
+
return await action();
|
|
445
|
+
}
|
|
446
|
+
finally {
|
|
447
|
+
release();
|
|
448
|
+
if (processPathMutationLocks.get(key) === current)
|
|
449
|
+
processPathMutationLocks.delete(key);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
exports.FileWriteObservationGate = FileWriteObservationGate;
|
|
454
|
+
//# sourceMappingURL=fileWriteObservation.js.map
|
|
@@ -12,7 +12,7 @@ export interface ToolPolicyDecision {
|
|
|
12
12
|
settingsVisible: boolean;
|
|
13
13
|
reason?: string;
|
|
14
14
|
}
|
|
15
|
-
export declare const PLAN_COMPUTER_USE_ACTIONS: readonly ["observe", "app_list", "app_observe"];
|
|
15
|
+
export declare const PLAN_COMPUTER_USE_ACTIONS: readonly ["observe", "app_list", "app_observe", "wait_for"];
|
|
16
16
|
export declare const PLAN_BROWSER_USE_ACTIONS: readonly ["observe", "navigate", "wait", "extract"];
|
|
17
17
|
/** 判断一个工具是否可参与并行调度。缺省 false(独占)。
|
|
18
18
|
* 优先看 toolchain registry 推断的 riskLevel('read' 工具天然并发安全,
|
package/dist/core/toolPolicy.js
CHANGED
|
@@ -78,7 +78,7 @@ const PLAN_READ_ONLY_TOOLS = new Set([
|
|
|
78
78
|
'branch_read',
|
|
79
79
|
'question',
|
|
80
80
|
]);
|
|
81
|
-
exports.PLAN_COMPUTER_USE_ACTIONS = ['observe', 'app_list', 'app_observe'];
|
|
81
|
+
exports.PLAN_COMPUTER_USE_ACTIONS = ['observe', 'app_list', 'app_observe', 'wait_for'];
|
|
82
82
|
exports.PLAN_BROWSER_USE_ACTIONS = ['observe', 'navigate', 'wait', 'extract'];
|
|
83
83
|
const PLAN_COMPUTER_USE_ACTION_SET = new Set(exports.PLAN_COMPUTER_USE_ACTIONS);
|
|
84
84
|
const PLAN_BROWSER_USE_ACTION_SET = new Set(exports.PLAN_BROWSER_USE_ACTIONS);
|