intentdna 1.9.3 → 1.9.5
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/cli/commands/run.js +26 -2
- package/dist/cli/index.js +1 -1
- package/dist/compiler/compile.js +9 -0
- package/dist/compiler/input-resolver.d.ts +2 -1
- package/dist/compiler/input-resolver.js +3 -30
- package/dist/hooks/cli.js +30 -12
- package/dist/hooks/enforce.js +8 -1
- package/dist/project-config.d.ts +2 -0
- package/dist/project-config.js +71 -0
- package/dist/runtime/canonical-run-service.js +4 -1
- package/dist/runtime/claude-sdk.js +4 -2
- package/dist/runtime/codex-adapter.js +1 -1
- package/dist/runtime/execution-provider.d.ts +5 -0
- package/dist/runtime/execution-provider.js +8 -0
- package/dist/runtime/index.d.ts +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/local-execution-authority.js +39 -4
- package/dist/runtime/providers/claude.d.ts +5 -0
- package/dist/runtime/providers/claude.js +61 -2
- package/dist/runtime/push-driver.d.ts +8 -1
- package/dist/runtime/push-driver.js +176 -5
- package/dist/runtime/result-store.js +69 -5
- package/dist/runtime/run-contracts.d.ts +34 -9
- package/dist/runtime/run-contracts.js +1 -1
- package/dist/runtime/worker-executor.js +14 -6
- package/dist/runtime/workspace-observation.d.ts +18 -3
- package/dist/runtime/workspace-observation.js +178 -20
- package/dist/schema/types.d.ts +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { constants, realpathSync } from "node:fs";
|
|
4
|
-
import { lstat, open, readdir,
|
|
4
|
+
import { lstat, open, readdir, readlink, realpath } from "node:fs/promises";
|
|
5
5
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
export class WorkspaceObservationError extends Error {
|
|
7
7
|
code;
|
|
@@ -12,10 +12,10 @@ export class WorkspaceObservationError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
export const DEFAULT_WORKSPACE_OBSERVATION_LIMITS = {
|
|
15
|
-
max_entries:
|
|
16
|
-
max_bytes:
|
|
15
|
+
max_entries: 250_000,
|
|
16
|
+
max_bytes: 8_589_934_592,
|
|
17
17
|
max_depth: 64,
|
|
18
|
-
max_duration_ms:
|
|
18
|
+
max_duration_ms: 120_000,
|
|
19
19
|
};
|
|
20
20
|
export const WORKSPACE_OBSERVATION_EXCLUDED_PATHS = [
|
|
21
21
|
".dna/runtime",
|
|
@@ -27,8 +27,7 @@ const GIT_EXCLUSION_PATHSPECS = [
|
|
|
27
27
|
":(exclude).dna/worktrees/runtime",
|
|
28
28
|
":(exclude).dna/worktrees/runtime/**",
|
|
29
29
|
];
|
|
30
|
-
function metadata(
|
|
31
|
-
const record = value;
|
|
30
|
+
function metadata(record) {
|
|
32
31
|
return {
|
|
33
32
|
dev: String(record.dev),
|
|
34
33
|
ino: String(record.ino),
|
|
@@ -38,6 +37,17 @@ function metadata(value) {
|
|
|
38
37
|
ctime_ns: String(record.ctimeNs),
|
|
39
38
|
};
|
|
40
39
|
}
|
|
40
|
+
function specialFileKind(value) {
|
|
41
|
+
if (value.isSocket())
|
|
42
|
+
return "socket";
|
|
43
|
+
if (value.isFIFO())
|
|
44
|
+
return "fifo";
|
|
45
|
+
if (value.isBlockDevice())
|
|
46
|
+
return "block_device";
|
|
47
|
+
if (value.isCharacterDevice())
|
|
48
|
+
return "character_device";
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
41
51
|
function sameMetadata(left, right) {
|
|
42
52
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
43
53
|
}
|
|
@@ -59,6 +69,29 @@ function git(cwd, args) {
|
|
|
59
69
|
});
|
|
60
70
|
return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
|
61
71
|
}
|
|
72
|
+
function sha256Text(value) {
|
|
73
|
+
return `sha256:${createHash("sha256").update(value, "utf8").digest("hex")}`;
|
|
74
|
+
}
|
|
75
|
+
function gitIgnoreEvidence(root, relativePath) {
|
|
76
|
+
const result = spawnSync("git", ["check-ignore", "--stdin", "-z", "-v"], {
|
|
77
|
+
cwd: root,
|
|
78
|
+
input: `${relativePath}\0`,
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
});
|
|
83
|
+
if (result.status === 1)
|
|
84
|
+
return null;
|
|
85
|
+
if (result.status !== 0) {
|
|
86
|
+
throw new WorkspaceObservationError("verification_failed", result.stderr?.trim() || `cannot inspect Git ignore status for ${relativePath}`);
|
|
87
|
+
}
|
|
88
|
+
const stdout = result.stdout ?? "";
|
|
89
|
+
const fields = stdout.split("\0");
|
|
90
|
+
if (fields.length !== 5 || fields[3] !== relativePath || fields[4] !== "") {
|
|
91
|
+
throw new WorkspaceObservationError("verification_failed", `Git returned invalid ignore evidence for ${relativePath}`);
|
|
92
|
+
}
|
|
93
|
+
return stdout;
|
|
94
|
+
}
|
|
62
95
|
function gitSnapshot(root) {
|
|
63
96
|
const top = git(root, ["rev-parse", "--show-toplevel"]);
|
|
64
97
|
if (top.status !== 0)
|
|
@@ -106,10 +139,12 @@ function validateLimits(input) {
|
|
|
106
139
|
}
|
|
107
140
|
export function workspaceObservationPolicy(limits) {
|
|
108
141
|
return {
|
|
109
|
-
schema_version: "intentdna.workspace_observation_policy.
|
|
142
|
+
schema_version: "intentdna.workspace_observation_policy.v4",
|
|
110
143
|
limits: validateLimits(limits),
|
|
111
144
|
excluded_relative_paths: WORKSPACE_OBSERVATION_EXCLUDED_PATHS,
|
|
112
145
|
git_observation: "head_branch_status_local_config",
|
|
146
|
+
symlink_observation: "reject_unignored_record_gitignored_target_as_opaque",
|
|
147
|
+
special_file_observation: "metadata_only_no_open",
|
|
113
148
|
};
|
|
114
149
|
}
|
|
115
150
|
export async function observeWorkspace(request) {
|
|
@@ -127,8 +162,13 @@ export async function observeWorkspace(request) {
|
|
|
127
162
|
const now = request.now_ms ?? Date.now;
|
|
128
163
|
const startedAt = now();
|
|
129
164
|
const digest = createHash("sha256");
|
|
165
|
+
const policy = workspaceObservationPolicy(limits);
|
|
166
|
+
digest.update(`policy\0${JSON.stringify(policy)}\0`);
|
|
130
167
|
let entries = 0;
|
|
131
168
|
let bytes = 0;
|
|
169
|
+
const opaqueExternalDependencies = [];
|
|
170
|
+
const opaqueChecks = [];
|
|
171
|
+
const gitBefore = gitSnapshot(root);
|
|
132
172
|
const enforce = (depth) => {
|
|
133
173
|
if (depth > limits.max_depth)
|
|
134
174
|
throw new WorkspaceObservationError("observation_limit_exceeded", "workspace depth limit exceeded");
|
|
@@ -144,16 +184,72 @@ export async function observeWorkspace(request) {
|
|
|
144
184
|
};
|
|
145
185
|
const visit = async (absolutePath, relativePath, depth) => {
|
|
146
186
|
enforce(depth);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
187
|
+
let beforeStatus;
|
|
188
|
+
try {
|
|
189
|
+
beforeStatus = await lstat(absolutePath, { bigint: true });
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
throw new WorkspaceObservationError("source_drift", `entry changed before observation: ${relativePath}`, { cause: error });
|
|
150
193
|
}
|
|
151
194
|
entries += 1;
|
|
152
195
|
enforce(depth);
|
|
153
196
|
const before = metadata(beforeStatus);
|
|
197
|
+
if (beforeStatus.isSymbolicLink()) {
|
|
198
|
+
if (gitBefore === null) {
|
|
199
|
+
throw new WorkspaceObservationError("verification_failed", `symlink is not observable: ${relativePath}`);
|
|
200
|
+
}
|
|
201
|
+
const ignoreEvidence = gitIgnoreEvidence(root, relativePath);
|
|
202
|
+
if (ignoreEvidence === null) {
|
|
203
|
+
throw new WorkspaceObservationError("verification_failed", `symlink is not observable: ${relativePath}`);
|
|
204
|
+
}
|
|
205
|
+
let linkTarget;
|
|
206
|
+
let resolvedTarget;
|
|
207
|
+
try {
|
|
208
|
+
linkTarget = await readlink(absolutePath);
|
|
209
|
+
resolvedTarget = await realpath(absolutePath);
|
|
210
|
+
await request.hooks?.after_metadata_read?.(relativePath);
|
|
211
|
+
const afterStatus = await lstat(absolutePath, { bigint: true });
|
|
212
|
+
const afterTarget = await readlink(absolutePath);
|
|
213
|
+
if (!afterStatus.isSymbolicLink()
|
|
214
|
+
|| !sameMetadata(before, metadata(afterStatus))
|
|
215
|
+
|| afterTarget !== linkTarget) {
|
|
216
|
+
throw new WorkspaceObservationError("source_drift", `symlink changed while observing: ${relativePath}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error instanceof WorkspaceObservationError)
|
|
221
|
+
throw error;
|
|
222
|
+
throw new WorkspaceObservationError("source_drift", `symlink changed while observing: ${relativePath}`, { cause: error });
|
|
223
|
+
}
|
|
224
|
+
const dependency = {
|
|
225
|
+
kind: "gitignored_symlink_target",
|
|
226
|
+
relative_path: relativePath,
|
|
227
|
+
link_target_digest: sha256Text(linkTarget),
|
|
228
|
+
resolved_target_path_digest: sha256Text(resolvedTarget),
|
|
229
|
+
git_ignore_evidence_digest: sha256Text(ignoreEvidence),
|
|
230
|
+
};
|
|
231
|
+
updateRecord("symlink", relativePath, before);
|
|
232
|
+
digest.update(`opaque_external_dependency\0${JSON.stringify(dependency)}\0`);
|
|
233
|
+
opaqueExternalDependencies.push(dependency);
|
|
234
|
+
opaqueChecks.push({
|
|
235
|
+
absolute_path: absolutePath,
|
|
236
|
+
relative_path: relativePath,
|
|
237
|
+
metadata: before,
|
|
238
|
+
link_target: linkTarget,
|
|
239
|
+
resolved_target_path_digest: dependency.resolved_target_path_digest,
|
|
240
|
+
git_ignore_evidence: ignoreEvidence,
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
154
244
|
if (beforeStatus.isDirectory()) {
|
|
155
245
|
updateRecord("directory", relativePath, before);
|
|
156
|
-
|
|
246
|
+
let names;
|
|
247
|
+
try {
|
|
248
|
+
names = (await readdir(absolutePath)).sort(canonicalNameOrder);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
throw new WorkspaceObservationError("source_drift", `directory changed before read: ${relativePath}`, { cause: error });
|
|
252
|
+
}
|
|
157
253
|
await request.hooks?.after_directory_read?.(relativePath);
|
|
158
254
|
for (const name of names) {
|
|
159
255
|
const childRelative = relativePath === "." ? name : `${relativePath}/${name}`;
|
|
@@ -161,19 +257,48 @@ export async function observeWorkspace(request) {
|
|
|
161
257
|
continue;
|
|
162
258
|
await visit(join(absolutePath, name), childRelative, depth + 1);
|
|
163
259
|
}
|
|
164
|
-
|
|
165
|
-
|
|
260
|
+
let afterStatus;
|
|
261
|
+
try {
|
|
262
|
+
afterStatus = await lstat(absolutePath, { bigint: true });
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
throw new WorkspaceObservationError("source_drift", `directory changed while observing: ${relativePath}`, { cause: error });
|
|
266
|
+
}
|
|
267
|
+
if (!afterStatus.isDirectory() || !sameMetadata(before, metadata(afterStatus))) {
|
|
166
268
|
throw new WorkspaceObservationError("source_drift", `directory changed while observing: ${relativePath}`);
|
|
167
269
|
}
|
|
168
270
|
return;
|
|
169
271
|
}
|
|
170
272
|
if (!beforeStatus.isFile()) {
|
|
171
|
-
|
|
273
|
+
const kind = specialFileKind(beforeStatus);
|
|
274
|
+
if (kind === null) {
|
|
275
|
+
throw new WorkspaceObservationError("verification_failed", `unknown filesystem node is not observable: ${relativePath}`);
|
|
276
|
+
}
|
|
277
|
+
let afterStatus;
|
|
278
|
+
try {
|
|
279
|
+
await request.hooks?.after_metadata_read?.(relativePath);
|
|
280
|
+
afterStatus = await lstat(absolutePath, { bigint: true });
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
throw new WorkspaceObservationError("source_drift", `${kind} changed while observing: ${relativePath}`, { cause: error });
|
|
284
|
+
}
|
|
285
|
+
if (specialFileKind(afterStatus) !== kind || !sameMetadata(before, metadata(afterStatus))) {
|
|
286
|
+
throw new WorkspaceObservationError("source_drift", `${kind} changed while observing: ${relativePath}`);
|
|
287
|
+
}
|
|
288
|
+
updateRecord(kind, relativePath, before);
|
|
289
|
+
return;
|
|
172
290
|
}
|
|
173
291
|
// Node does not expose openat(2), so same-user namespace replacement cannot be
|
|
174
292
|
// eliminated completely. O_NOFOLLOW plus descriptor/path revalidation closes
|
|
175
293
|
// the practical symlink and replacement windows without native code.
|
|
176
|
-
|
|
294
|
+
await request.hooks?.after_metadata_read?.(relativePath);
|
|
295
|
+
let handle;
|
|
296
|
+
try {
|
|
297
|
+
handle = await open(absolutePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
throw new WorkspaceObservationError("source_drift", `file changed before read: ${relativePath}`, { cause: error });
|
|
301
|
+
}
|
|
177
302
|
try {
|
|
178
303
|
const descriptorBefore = metadata(await handle.stat({ bigint: true }));
|
|
179
304
|
if (!sameMetadata(before, descriptorBefore)) {
|
|
@@ -188,9 +313,17 @@ export async function observeWorkspace(request) {
|
|
|
188
313
|
digest.update(buffer);
|
|
189
314
|
}
|
|
190
315
|
digest.update("\0");
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
316
|
+
await request.hooks?.after_file_read?.(relativePath);
|
|
317
|
+
let descriptorAfter;
|
|
318
|
+
let pathAfter;
|
|
319
|
+
try {
|
|
320
|
+
descriptorAfter = metadata(await handle.stat({ bigint: true }));
|
|
321
|
+
pathAfter = await lstat(absolutePath, { bigint: true });
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
throw new WorkspaceObservationError("source_drift", `file changed while observing: ${relativePath}`, { cause: error });
|
|
325
|
+
}
|
|
326
|
+
if (!pathAfter.isFile() || !sameMetadata(before, descriptorAfter) || !sameMetadata(before, metadata(pathAfter))) {
|
|
194
327
|
throw new WorkspaceObservationError("source_drift", `file changed while observing: ${relativePath}`);
|
|
195
328
|
}
|
|
196
329
|
}
|
|
@@ -198,19 +331,44 @@ export async function observeWorkspace(request) {
|
|
|
198
331
|
await handle.close();
|
|
199
332
|
}
|
|
200
333
|
};
|
|
201
|
-
const gitBefore = gitSnapshot(root);
|
|
202
334
|
await visit(root, ".", 0);
|
|
335
|
+
for (const check of opaqueChecks) {
|
|
336
|
+
try {
|
|
337
|
+
const status = await lstat(check.absolute_path, { bigint: true });
|
|
338
|
+
const linkTarget = await readlink(check.absolute_path);
|
|
339
|
+
const resolvedTarget = await realpath(check.absolute_path);
|
|
340
|
+
const ignoreEvidence = gitIgnoreEvidence(root, check.relative_path);
|
|
341
|
+
if (!status.isSymbolicLink()
|
|
342
|
+
|| !sameMetadata(check.metadata, metadata(status))
|
|
343
|
+
|| linkTarget !== check.link_target
|
|
344
|
+
|| sha256Text(resolvedTarget) !== check.resolved_target_path_digest
|
|
345
|
+
|| ignoreEvidence !== check.git_ignore_evidence) {
|
|
346
|
+
throw new WorkspaceObservationError("source_drift", `opaque external dependency changed while observing: ${check.relative_path}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (error instanceof WorkspaceObservationError)
|
|
351
|
+
throw error;
|
|
352
|
+
throw new WorkspaceObservationError("source_drift", `opaque external dependency changed while observing: ${check.relative_path}`, { cause: error });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
opaqueExternalDependencies.sort((left, right) => canonicalNameOrder(left.relative_path, right.relative_path));
|
|
356
|
+
digest.update(`scope\0${JSON.stringify({
|
|
357
|
+
excluded_relative_paths: WORKSPACE_OBSERVATION_EXCLUDED_PATHS,
|
|
358
|
+
opaque_external_dependencies: opaqueExternalDependencies,
|
|
359
|
+
})}\0`);
|
|
203
360
|
const gitAfter = gitSnapshot(root);
|
|
204
361
|
if (JSON.stringify(gitBefore) !== JSON.stringify(gitAfter)) {
|
|
205
362
|
throw new WorkspaceObservationError("source_drift", "Git workspace state changed while observing");
|
|
206
363
|
}
|
|
207
364
|
digest.update(`git\0${JSON.stringify(gitAfter)}\0`);
|
|
208
365
|
return {
|
|
209
|
-
schema_version: "intentdna.workspace_observation.
|
|
366
|
+
schema_version: "intentdna.workspace_observation.v5",
|
|
210
367
|
observed_digest: `sha256:${digest.digest("hex")}`,
|
|
211
368
|
entry_count: entries,
|
|
212
369
|
byte_count: bytes,
|
|
213
370
|
excluded_relative_paths: WORKSPACE_OBSERVATION_EXCLUDED_PATHS,
|
|
371
|
+
opaque_external_dependencies: opaqueExternalDependencies,
|
|
214
372
|
git: gitAfter,
|
|
215
373
|
};
|
|
216
374
|
}
|
package/dist/schema/types.d.ts
CHANGED