entroly-wasm 1.0.78 → 1.0.80

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.
@@ -0,0 +1,151 @@
1
+ 'use strict';
2
+
3
+ // Node orchestration-only content identity for passive Work Graph snapshots.
4
+ // Rust decides whether observations are semantically duplicate; this helper
5
+ // merely supplies exact Git blob identity where it can do so without guessing.
6
+ // It never follows symlinks, reads special files, or reads an unbounded file.
7
+ const crypto = require('crypto');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const { spawnSync } = require('child_process');
11
+
12
+ const MAX_HASH_FILE_BYTES = 64 * 1024 * 1024;
13
+ const HASH_CHUNK_BYTES = 1024 * 1024;
14
+
15
+ function gitEnv() {
16
+ return {
17
+ ...process.env,
18
+ GIT_TERMINAL_PROMPT: '0',
19
+ GIT_OPTIONAL_LOCKS: '0',
20
+ GIT_PAGER: 'cat',
21
+ LC_ALL: 'C',
22
+ LANG: 'C',
23
+ };
24
+ }
25
+
26
+ function runGitText(cwd, args) {
27
+ const result = spawnSync(
28
+ 'git',
29
+ [
30
+ '-c', 'core.fsmonitor=false',
31
+ '-c', 'core.untrackedCache=false',
32
+ '-c', 'submodule.recurse=false',
33
+ '-C', String(cwd),
34
+ ...args,
35
+ ],
36
+ {
37
+ encoding: 'utf8',
38
+ env: gitEnv(),
39
+ maxBuffer: 1024 * 1024,
40
+ timeout: 5000,
41
+ windowsHide: true,
42
+ },
43
+ );
44
+ if (result.error || result.status !== 0) return '';
45
+ return String(result.stdout || '').trim();
46
+ }
47
+
48
+ function resolveRoot(repoPath) {
49
+ const candidate = path.resolve(String(repoPath || '.'));
50
+ const output = runGitText(candidate, ['rev-parse', '--show-toplevel']);
51
+ if (!output) throw new Error(`not a Git worktree: ${candidate}`);
52
+ return path.resolve(output);
53
+ }
54
+
55
+ function objectHashAlgorithm(root) {
56
+ const value = runGitText(root, ['rev-parse', '--show-object-format']).toLowerCase();
57
+ return value === 'sha1' || value === 'sha256' ? value : '';
58
+ }
59
+
60
+ function relativeRegularPath(root, repoRel) {
61
+ const value = String(repoRel || '').replace(/\\/g, '/');
62
+ if (!value || value.startsWith('/') || /[\0\r\n]/.test(value)) return null;
63
+ const parts = value.split('/');
64
+ if (parts.some(part => !part || part === '.' || part === '..')) return null;
65
+ const candidate = path.join(root, ...parts);
66
+ let before;
67
+ try { before = fs.lstatSync(candidate, { bigint: true }); }
68
+ catch (_) { return null; }
69
+ if (!before.isFile() || before.size > BigInt(MAX_HASH_FILE_BYTES)) return null;
70
+ return { candidate, before };
71
+ }
72
+
73
+ function sameFile(left, right) {
74
+ if (!left.isFile() || !right.isFile()) return false;
75
+ // `dev` is compared only when both sides report one. Node on Windows returns
76
+ // dev=0 from lstat but a real device id from fstat for the same file, so
77
+ // requiring strict equality rejected every legitimate file and made
78
+ // gitBlobDigest return '' for all input -- content identity was silently
79
+ // dead on win32, which is the binding handoff/resume rely on. The anti-swap
80
+ // property is preserved: the file index (ino), size, and both timestamps
81
+ // must still match across lstat -> open -> lstat.
82
+ if (left.dev !== 0n && right.dev !== 0n && left.dev !== right.dev) return false;
83
+ return left.ino === right.ino
84
+ && left.size === right.size
85
+ && left.mtimeNs === right.mtimeNs
86
+ && left.ctimeNs === right.ctimeNs;
87
+ }
88
+
89
+ function gitBlobDigest(root, repoRel, algorithm) {
90
+ const safe = relativeRegularPath(root, repoRel);
91
+ if (!safe) return '';
92
+
93
+ let fd;
94
+ try {
95
+ let flags = fs.constants.O_RDONLY;
96
+ if (typeof fs.constants.O_NOFOLLOW === 'number') flags |= fs.constants.O_NOFOLLOW;
97
+ fd = fs.openSync(safe.candidate, flags);
98
+ const opened = fs.fstatSync(fd, { bigint: true });
99
+ const pathAfterOpen = fs.lstatSync(safe.candidate, { bigint: true });
100
+ if (!sameFile(safe.before, opened) || !sameFile(opened, pathAfterOpen)) return '';
101
+ if (opened.size > BigInt(MAX_HASH_FILE_BYTES)) return '';
102
+
103
+ const hash = crypto.createHash(algorithm);
104
+ hash.update(Buffer.from(`blob ${opened.size.toString()}\0`, 'ascii'));
105
+ const buffer = Buffer.allocUnsafe(HASH_CHUNK_BYTES);
106
+ let offset = 0n;
107
+ while (offset < opened.size) {
108
+ const remaining = opened.size - offset;
109
+ const length = Number(remaining > BigInt(buffer.length) ? BigInt(buffer.length) : remaining);
110
+ const bytes = fs.readSync(fd, buffer, 0, length, Number(offset));
111
+ if (bytes <= 0) return '';
112
+ hash.update(buffer.subarray(0, bytes));
113
+ offset += BigInt(bytes);
114
+ }
115
+
116
+ const after = fs.fstatSync(fd, { bigint: true });
117
+ const pathAfterRead = fs.lstatSync(safe.candidate, { bigint: true });
118
+ if (!sameFile(opened, after) || !sameFile(after, pathAfterRead)) return '';
119
+ return `git-blob:${hash.digest('hex')}`;
120
+ } catch (_) {
121
+ return '';
122
+ } finally {
123
+ if (fd !== undefined) {
124
+ try { fs.closeSync(fd); } catch (_) {}
125
+ }
126
+ }
127
+ }
128
+
129
+ function enrichWorktreeContentDigests(repoPath, observation) {
130
+ const changes = observation && observation.changes;
131
+ if (!Array.isArray(changes)) return observation;
132
+ const root = resolveRoot(repoPath);
133
+ const algorithm = objectHashAlgorithm(root);
134
+
135
+ for (const change of changes) {
136
+ if (!change || typeof change !== 'object') continue;
137
+ change.content_digest = '';
138
+ if (change.staged || change.conflicted) continue;
139
+ if (String(change.kind || '') === 'deleted') {
140
+ change.content_digest = 'worktree:deleted';
141
+ continue;
142
+ }
143
+ if (!algorithm) continue;
144
+ change.content_digest = gitBlobDigest(root, change.path, algorithm);
145
+ }
146
+ return observation;
147
+ }
148
+
149
+ module.exports = {
150
+ enrichWorktreeContentDigests,
151
+ };
@@ -0,0 +1,54 @@
1
+ import type {
2
+ WorkContinuationProof,
3
+ WorkGraphHandoffReceipt,
4
+ WorkGraphResumeView,
5
+ } from "./work_graph";
6
+ import type { RepositoryDiscoveryOptions } from "./work_graph_repo";
7
+ import type { WorkGraphStoreOptions } from "./work_graph_store";
8
+
9
+ export interface ResumeRepositoryOptions {
10
+ workstreamId?: string | null;
11
+ maxEvidence?: number;
12
+ storeOptions?: WorkGraphStoreOptions;
13
+ repositoryOptions?: RepositoryDiscoveryOptions;
14
+ }
15
+
16
+ export interface HandoffRepositoryOptions {
17
+ workstreamId: string;
18
+ fromAgent: string;
19
+ toAgent: string;
20
+ generatedAtMs?: number;
21
+ storeOptions?: WorkGraphStoreOptions;
22
+ repositoryOptions?: RepositoryDiscoveryOptions;
23
+ }
24
+
25
+ export const MAX_RESUME_EVIDENCE: number;
26
+ export const MAX_WORK_ID_CHARS: number;
27
+
28
+ /**
29
+ * Refresh bounded durable repository/checkpoint facts and recover unfinished
30
+ * work from the shared Rust Work Graph. The previous agent does not need to
31
+ * have produced an explicit handoff.
32
+ */
33
+ export function resumeRepository(
34
+ repoPath?: string,
35
+ options?: ResumeRepositoryOptions,
36
+ ): WorkGraphResumeView;
37
+
38
+ /** Refresh durable facts exactly once, then seal a graph-bound handoff receipt. */
39
+ export function handoffRepository(
40
+ repoPath: string | undefined,
41
+ options: HandoffRepositoryOptions,
42
+ ): WorkGraphHandoffReceipt;
43
+
44
+ /**
45
+ * Refresh durable facts once and return both the compatibility handoff receipt
46
+ * and the Rust-verified complete continuation proof.
47
+ */
48
+ export function handoffRepositoryWithProof(
49
+ repoPath: string | undefined,
50
+ options: HandoffRepositoryOptions,
51
+ ): {
52
+ handoff: WorkGraphHandoffReceipt;
53
+ continuation_proof: WorkContinuationProof;
54
+ };
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ // Thin Node orchestration for interrupted-agent recovery and fresh handoff.
4
+ // Repository observation and persistence happen here; all work-state meaning
5
+ // remains in shared Rust.
6
+ const { enrichWorktreeContentDigests } = require('./work_graph_content_digest');
7
+ const { discoverRepositoryObservation } = require('./work_graph_repo');
8
+ const { WorkGraphStateError, WorkGraphStore } = require('./work_graph_store');
9
+
10
+ const MAX_RESUME_EVIDENCE = 4096;
11
+ const MAX_WORK_ID_CHARS = 512;
12
+
13
+ function validateMaxEvidence(value) {
14
+ const number = Number(value);
15
+ if (!Number.isSafeInteger(number) || number < 0 || number > MAX_RESUME_EVIDENCE) {
16
+ throw new WorkGraphStateError(
17
+ `maxEvidence must be a safe integer between 0 and ${MAX_RESUME_EVIDENCE}`,
18
+ );
19
+ }
20
+ return number;
21
+ }
22
+
23
+ function requiredId(value, name) {
24
+ const text = String(value == null ? '' : value).trim();
25
+ if (!text) throw new WorkGraphStateError(`${name} must not be empty`);
26
+ if (text.length > MAX_WORK_ID_CHARS || text.includes('\0')) {
27
+ throw new WorkGraphStateError(
28
+ `${name} may not exceed ${MAX_WORK_ID_CHARS} characters or contain NUL`,
29
+ );
30
+ }
31
+ return text;
32
+ }
33
+
34
+ function optionalWorkstreamId(value) {
35
+ if (value == null || value === '') return null;
36
+ const text = String(value).trim();
37
+ if (text.length > MAX_WORK_ID_CHARS || text.includes('\0')) {
38
+ throw new WorkGraphStateError(
39
+ `workstreamId may not exceed ${MAX_WORK_ID_CHARS} characters or contain NUL`,
40
+ );
41
+ }
42
+ return text || null;
43
+ }
44
+
45
+ function passiveObservation(repoPath, repositoryOptions) {
46
+ const observation = discoverRepositoryObservation(repoPath, repositoryOptions);
47
+ return enrichWorktreeContentDigests(repoPath, observation);
48
+ }
49
+
50
+ function resumeRepository(repoPath = '.', options = {}) {
51
+ // Validate before constructing the store or observing Git. Invalid recovery
52
+ // requests must not mutate shared state.
53
+ const maxEvidence = validateMaxEvidence(options.maxEvidence ?? 128);
54
+ const workstreamId = optionalWorkstreamId(options.workstreamId);
55
+ const storeOptions = options.storeOptions || {};
56
+ const repositoryOptions = options.repositoryOptions || {};
57
+
58
+ const store = WorkGraphStore.forRepository(repoPath, storeOptions);
59
+ store.submitObservation(passiveObservation(repoPath, repositoryOptions));
60
+ return store.resume(workstreamId, maxEvidence);
61
+ }
62
+
63
+ function handoffRepository(repoPath = '.', options = {}) {
64
+ // A receipt is a state-sealing operation. Validate everything before touching
65
+ // disk, then refresh the bounded durable facts exactly once before Rust seals
66
+ // the handoff against the graph commitment.
67
+ const workstreamId = requiredId(options.workstreamId, 'workstreamId');
68
+ const fromAgent = requiredId(options.fromAgent, 'fromAgent');
69
+ const toAgent = requiredId(options.toAgent, 'toAgent');
70
+ const storeOptions = options.storeOptions || {};
71
+ const repositoryOptions = options.repositoryOptions || {};
72
+ const generatedAtMs = options.generatedAtMs == null ? Date.now() : Number(options.generatedAtMs);
73
+ if (!Number.isSafeInteger(generatedAtMs)) {
74
+ throw new WorkGraphStateError('generatedAtMs must be a JavaScript-safe integer');
75
+ }
76
+
77
+ const store = WorkGraphStore.forRepository(repoPath, storeOptions);
78
+ store.submitObservation(passiveObservation(repoPath, repositoryOptions));
79
+ return store.handoff(workstreamId, fromAgent, toAgent, generatedAtMs);
80
+ }
81
+
82
+ function continuationOutstandingRefs(view) {
83
+ const safeView = view && typeof view === 'object' ? view : {};
84
+ const selected = safeView.selected_workstream && typeof safeView.selected_workstream === 'object'
85
+ ? safeView.selected_workstream
86
+ : {};
87
+ const values = [
88
+ ...(Array.isArray(safeView.changed_paths) ? safeView.changed_paths : []),
89
+ ...(Array.isArray(safeView.failures) ? safeView.failures : []),
90
+ ...(Array.isArray(selected.task_ids) ? selected.task_ids : []),
91
+ ];
92
+ return [...new Set(values
93
+ .filter((value) => typeof value === 'string' && value.trim())
94
+ .map((value) => value.trim()))];
95
+ }
96
+
97
+ function handoffRepositoryWithProof(repoPath = '.', options = {}) {
98
+ // Preserve the receipt-only API above for compatibility while making the
99
+ // complete continuation artifact available as one low-ceremony operation.
100
+ const workstreamId = requiredId(options.workstreamId, 'workstreamId');
101
+ const fromAgent = requiredId(options.fromAgent, 'fromAgent');
102
+ const toAgent = requiredId(options.toAgent, 'toAgent');
103
+ const storeOptions = options.storeOptions || {};
104
+ const repositoryOptions = options.repositoryOptions || {};
105
+ const generatedAtMs = options.generatedAtMs == null ? Date.now() : Number(options.generatedAtMs);
106
+ if (!Number.isSafeInteger(generatedAtMs)) {
107
+ throw new WorkGraphStateError('generatedAtMs must be a JavaScript-safe integer');
108
+ }
109
+
110
+ const store = WorkGraphStore.forRepository(repoPath, storeOptions);
111
+ store.submitObservation(passiveObservation(repoPath, repositoryOptions));
112
+ const handoff = store.handoff(workstreamId, fromAgent, toAgent, generatedAtMs);
113
+ const view = store.resume(workstreamId, 128);
114
+ const outstanding = continuationOutstandingRefs(view);
115
+ const continuationProof = store.continuationProof(handoff, {
116
+ outstanding_work_refs: outstanding,
117
+ created_at_ms: generatedAtMs,
118
+ });
119
+ return { handoff, continuation_proof: continuationProof };
120
+ }
121
+
122
+ module.exports = {
123
+ MAX_RESUME_EVIDENCE,
124
+ MAX_WORK_ID_CHARS,
125
+ handoffRepository,
126
+ handoffRepositoryWithProof,
127
+ resumeRepository,
128
+ };
@@ -0,0 +1,26 @@
1
+ import type { WorkGraphObservation } from "./work_graph";
2
+
3
+ export interface RepositoryDiscoveryOptions {
4
+ agentId?: string;
5
+ sessionId?: string;
6
+ taskHint?: Record<string, unknown> | null;
7
+ defaultBranch?: string;
8
+ maxCommits?: number;
9
+ observedAtMs?: number;
10
+ includeCheckpoint?: boolean;
11
+ checkpointDir?: string;
12
+ }
13
+
14
+ export interface RepositoryIdentity {
15
+ repo_id: string;
16
+ root: string;
17
+ }
18
+
19
+ export class RepositoryDiscoveryError extends Error {}
20
+
21
+ export function discoverRepositoryIdentity(path?: string): RepositoryIdentity;
22
+
23
+ export function discoverRepositoryObservation(
24
+ path?: string,
25
+ options?: RepositoryDiscoveryOptions,
26
+ ): WorkGraphObservation;