@quolu/lattice 0.52.0 → 0.52.2
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/LICENSE +147 -147
- package/README.ja.md +355 -355
- package/README.md +258 -258
- package/bin/lattice-mcp.mjs +0 -0
- package/bin/lattice-scripted-adapter.mjs +0 -0
- package/bin/lattice-scripted-worker.mjs +0 -0
- package/bin/lattice-work-order-adapter.mjs +0 -0
- package/bin/lattice.mjs +0 -0
- package/docs/bridge-setup.md +132 -132
- package/docs/schemas/lattice.executor_packet.v1.schema.json +57 -57
- package/docs/schemas/lattice.executor_receipt.v1.schema.json +66 -66
- package/docs/schemas/lattice.phase_todo_revision.v3.schema.json +360 -360
- package/docs/schemas/lattice.plan_create_input.v1.schema.json +56 -56
- package/docs/schemas/lattice.plan_create_input.v2.schema.json +72 -72
- package/docs/schemas/lattice.plan_create_input.v3.schema.json +81 -81
- package/docs/schemas/lattice.plan_create_input.v4.schema.json +85 -85
- package/docs/schemas/lattice.run_request.v1.schema.json +238 -238
- package/docs/schemas/lattice.runtime_adapter_capabilities.v2.schema.json +55 -55
- package/docs/schemas/lattice.runtime_adapter_registration_input.v1.schema.json +78 -78
- package/docs/schemas/lattice.runtime_adapter_registration_input.v2.schema.json +86 -86
- package/docs/schemas/lattice.todo_extraction.v2.schema.json +298 -298
- package/docs/schemas/lattice.todo_extraction.v3.schema.json +146 -146
- package/docs/schemas/lattice.todo_revision.v2.schema.json +260 -260
- package/docs/schemas/lattice.todo_revision_set.v3.schema.json +363 -363
- package/package.json +103 -103
- package/sensor/LICENSE +21 -21
- package/sensor/NOTICE +19 -19
- package/sensor/dist/bin/lattice-sensor.js +9 -9
- package/sensor/dist/db/index.js +24 -24
- package/sensor/dist/db/migrations.js +41 -41
- package/sensor/dist/db/queries.js +164 -164
- package/sensor/dist/db/schema.sql +205 -205
- package/sensor/dist/directory.js +5 -5
- package/sensor/dist/extraction/wasm/tree-sitter-c_sharp.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfml.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfquery.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cfscript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-cobol.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-erlang.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-go.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-java.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-javascript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-nix.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-python.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-tsx.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-typescript.wasm +0 -0
- package/sensor/dist/extraction/wasm/tree-sitter-vbnet.wasm +0 -0
- package/sensor/dist/mcp/liveness-watchdog.js +53 -53
- package/sensor/dist/mcp/server-instructions.js +95 -95
- package/sensor/package.json +56 -56
- package/src/artifact-contracts-v2.mjs +325 -325
- package/src/artifact-contracts.mjs +895 -895
- package/src/boundary-compiler.mjs +712 -712
- package/src/boundary-observation-compiler-v2.mjs +344 -344
- package/src/bounded-seam.mjs +230 -230
- package/src/bridge-address.mjs +107 -107
- package/src/bridge-cli.mjs +297 -297
- package/src/bridge-config.mjs +346 -346
- package/src/bridge-daemon.mjs +378 -378
- package/src/bridge-launch-agent.mjs +323 -323
- package/src/bridge-registrar.mjs +102 -102
- package/src/bridge-server.mjs +376 -376
- package/src/cli-help.mjs +308 -308
- package/src/cli-stdio.mjs +40 -40
- package/src/control-compiler.mjs +532 -532
- package/src/dag-chain.mjs +261 -261
- package/src/factory-diagnostics.mjs +188 -188
- package/src/hash-chain.mjs +76 -76
- package/src/hooks-cli.mjs +1057 -1053
- package/src/isolation-runner.mjs +409 -409
- package/src/node-version-guard.mjs +44 -44
- package/src/project-cli.mjs +697 -697
- package/src/project-identity.mjs +130 -130
- package/src/rc1-black-box-oracle.mjs +906 -906
- package/src/rc1-comparison.mjs +154 -154
- package/src/rc1-evidence-bundle.mjs +456 -456
- package/src/rc1-v4-campaign.mjs +708 -708
- package/src/rc1-v4-transform.mjs +467 -467
- package/src/rc1-v5-artifact-set.mjs +855 -855
- package/src/rc1-v5-behavior-evidence.mjs +594 -594
- package/src/rc1-v5-campaign.mjs +797 -797
- package/src/rc1-v5-transform.mjs +421 -421
- package/src/rc1-v6-artifact-set.mjs +807 -807
- package/src/rc1-v6-behavior-evidence.mjs +273 -273
- package/src/rc1-v6-campaign.mjs +623 -623
- package/src/rc1-v6-causal-binding.mjs +473 -473
- package/src/rc1-v6-measurement.mjs +313 -313
- package/src/rc2-artifact-set.mjs +1584 -1584
- package/src/rc2-campaign.mjs +1506 -1502
- package/src/rc2-delivery-policy-front-end.mjs +1079 -1079
- package/src/rc2-delivery-policy-oracle.mjs +134 -134
- package/src/rc2-delivery-policy-transform.mjs +1127 -1127
- package/src/rc2-rc1-transfer-front-end.mjs +511 -511
- package/src/rc3-actual-dogfood.mjs +652 -652
- package/src/rc3-dogfood-scaffold.mjs +315 -315
- package/src/rc3-scripted-campaign.mjs +1383 -1383
- package/src/rc4-stage1-dogfood.mjs +673 -673
- package/src/runtime-adapter-registry.mjs +524 -520
- package/src/runtime-cli.mjs +4588 -4584
- package/src/runtime-contracts.mjs +824 -824
- package/src/runtime-control-store.mjs +604 -600
- package/src/runtime-controller-protocol.mjs +587 -587
- package/src/runtime-decision-verifier.mjs +701 -701
- package/src/runtime-diff-observer.mjs +361 -361
- package/src/runtime-direct-os-observer.mjs +301 -301
- package/src/runtime-driver-state.mjs +166 -162
- package/src/runtime-engine.mjs +779 -779
- package/src/runtime-errors.mjs +356 -356
- package/src/runtime-event-store.mjs +189 -189
- package/src/runtime-front-end.mjs +925 -925
- package/src/runtime-gate-store.mjs +481 -477
- package/src/runtime-hold-recompile.mjs +916 -916
- package/src/runtime-io-sentinel.mjs +391 -391
- package/src/runtime-lifecycle-lock.mjs +294 -290
- package/src/runtime-managed-supervisor.mjs +1490 -1490
- package/src/runtime-multi-epoch-store.mjs +838 -834
- package/src/runtime-projection.mjs +269 -269
- package/src/runtime-pull-intake.mjs +1192 -1188
- package/src/runtime-scripted-adapter-controller.mjs +1160 -1156
- package/src/runtime-scripted-executor.mjs +163 -163
- package/src/runtime-scripted-worktree.mjs +104 -104
- package/src/runtime-seam-resolve.mjs +428 -428
- package/src/runtime-seam-treatment.mjs +173 -173
- package/src/runtime-socket-owner.mjs +125 -125
- package/src/runtime-work-order-contracts.mjs +91 -91
- package/src/runtime-work-order-controller.mjs +1171 -1167
- package/src/runtime-worktree-executor.mjs +199 -199
- package/src/schedulability-compiler-v2.mjs +303 -303
- package/src/schedulability-verifier-v2.mjs +317 -317
- package/src/seam-apply.mjs +549 -549
- package/src/seam-commit-shared.mjs +22 -22
- package/src/seam-commit-transform.mjs +81 -81
- package/src/seam-commit.mjs +18 -18
- package/src/seam-cost.mjs +322 -322
- package/src/seam-derivation.mjs +188 -188
- package/src/seam-gate.mjs +146 -146
- package/src/seam-proposal-contracts.mjs +446 -446
- package/src/seam-proposal-queries.mjs +521 -521
- package/src/seam-proposal.mjs +2011 -2011
- package/src/seam-ref.mjs +33 -33
- package/src/seam-rewrite.mjs +286 -286
- package/src/seam-transform.mjs +554 -554
- package/src/seam-verification.mjs +260 -260
- package/src/sensor-adapter.mjs +432 -432
- package/src/sensor-cli.mjs +139 -139
- package/src/sensor-diff.mjs +661 -661
- package/src/sensor-node-runtime.mjs +53 -53
- package/src/sensor-runtime.mjs +52 -52
- package/src/timestamp-contract.mjs +8 -8
- package/src/todo-audit-pending.mjs +91 -91
- package/src/todo-chain.mjs +178 -178
- package/src/todo-cli.mjs +3141 -3126
- package/src/todo-contracts.mjs +728 -728
- package/src/todo-dashboard-registry.mjs +573 -573
- package/src/todo-dispatch-shape.mjs +190 -190
- package/src/todo-gantt-html-independence.mjs +239 -239
- package/src/todo-gantt-html-shared.mjs +226 -226
- package/src/todo-gantt-html-style.mjs +131 -131
- package/src/todo-gantt-html.mjs +248 -248
- package/src/todo-gantt-layout.mjs +974 -974
- package/src/todo-gantt-live.mjs +361 -361
- package/src/todo-gantt-nested.mjs +263 -263
- package/src/todo-gantt-presentation.mjs +217 -217
- package/src/todo-gantt-scope.mjs +123 -123
- package/src/todo-gantt-svg.mjs +353 -353
- package/src/todo-independence-contracts.mjs +595 -595
- package/src/todo-independence-guidance.mjs +322 -322
- package/src/todo-independence.mjs +640 -640
- package/src/todo-markdown-renderer.mjs +260 -260
- package/src/todo-migration.mjs +448 -448
- package/src/todo-narrative-anchor.mjs +130 -130
- package/src/todo-note-store.mjs +629 -625
- package/src/todo-parallel-candidates.mjs +114 -114
- package/src/todo-revision.mjs +995 -995
- package/src/todo-split.mjs +472 -472
- package/src/todo-status.mjs +690 -690
- package/src/todo-store-git-transaction.mjs +418 -418
- package/src/todo-store.mjs +4322 -4314
- package/src/treatment-compiler.mjs +728 -728
- package/src/treatment-runner.mjs +656 -656
- package/src/witness-scaffold.mjs +180 -180
|
@@ -1,1156 +1,1160 @@
|
|
|
1
|
-
import net from 'node:net';
|
|
2
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
-
import { execFile, spawn } from 'node:child_process';
|
|
4
|
-
import { constants as fsConstants, readFileSync } from 'node:fs';
|
|
5
|
-
import {
|
|
6
|
-
chmod,
|
|
7
|
-
lstat,
|
|
8
|
-
mkdir,
|
|
9
|
-
open,
|
|
10
|
-
readFile,
|
|
11
|
-
realpath,
|
|
12
|
-
rename,
|
|
13
|
-
rm,
|
|
14
|
-
} from 'node:fs/promises';
|
|
15
|
-
import path from 'node:path';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
|
-
import { promisify } from 'node:util';
|
|
18
|
-
|
|
19
|
-
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
20
|
-
import {
|
|
21
|
-
armStagedWriteLease,
|
|
22
|
-
CONTROLLER_OPERATIONS,
|
|
23
|
-
validateArmedWriteLease,
|
|
24
|
-
validateControllerRequest,
|
|
25
|
-
validateRuntimeAdapterCapabilities,
|
|
26
|
-
validateRuntimeHeartbeatPolicy,
|
|
27
|
-
} from './runtime-controller-protocol.mjs';
|
|
28
|
-
import {
|
|
29
|
-
selfDigest,
|
|
30
|
-
validateExecutorPacket,
|
|
31
|
-
validateExecutorReceipt,
|
|
32
|
-
} from './runtime-contracts.mjs';
|
|
33
|
-
import { captureWorktreeDiff } from './runtime-diff-observer.mjs';
|
|
34
|
-
import { validateSupervisorWriteGate } from './runtime-gate-store.mjs';
|
|
35
|
-
import { observeManagedProcessStartIdentity } from './runtime-managed-supervisor.mjs';
|
|
36
|
-
import { scriptedWorktreeId, scriptedWorktreePath } from './runtime-scripted-worktree.mjs';
|
|
37
|
-
|
|
38
|
-
const MAX_DOCUMENT_BYTES = 8_388_608;
|
|
39
|
-
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
40
|
-
const ID = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/u;
|
|
41
|
-
const execFileAsync = promisify(execFile);
|
|
42
|
-
const REQUEST_SCHEMA_TO_OPERATION = Object.freeze(Object.fromEntries([
|
|
43
|
-
['lattice.adapter_dispatch_request.v1', 'dispatch'],
|
|
44
|
-
['lattice.adapter_observe_request.v1', 'observe'],
|
|
45
|
-
['lattice.adapter_running_inventory_request.v1', 'inventory'],
|
|
46
|
-
['lattice.adapter_barrier_request.v1', 'barrier'],
|
|
47
|
-
['lattice.adapter_rebind_request.v1', 'rebind'],
|
|
48
|
-
['lattice.adapter_prepare_request.v1', 'prepare'],
|
|
49
|
-
['lattice.adapter_activate_request.v1', 'activate'],
|
|
50
|
-
['lattice.adapter_release_request.v1', 'release'],
|
|
51
|
-
['lattice.adapter_revoke_request.v1', 'revoke'],
|
|
52
|
-
]));
|
|
53
|
-
|
|
54
|
-
export class ScriptedAdapterControllerError extends Error {
|
|
55
|
-
constructor(code, message, detail = {}) {
|
|
56
|
-
super(message);
|
|
57
|
-
this.name = 'ScriptedAdapterControllerError';
|
|
58
|
-
this.code = code;
|
|
59
|
-
this.detail = detail;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function fail(code, message, detail) {
|
|
64
|
-
throw new ScriptedAdapterControllerError(code, message, detail);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function plain(value) {
|
|
68
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
69
|
-
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function exact(value, fields) {
|
|
73
|
-
if (!plain(value)) return false;
|
|
74
|
-
const actual = Object.keys(value).sort();
|
|
75
|
-
const expected = [...fields].sort();
|
|
76
|
-
return actual.length === expected.length
|
|
77
|
-
&& actual.every((field, index) => field === expected[index]);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function sign(value, field) {
|
|
81
|
-
value[field] = '';
|
|
82
|
-
value[field] = selfDigest(value, field);
|
|
83
|
-
return value;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function sha256Bytes(bytes) {
|
|
87
|
-
return createHash('sha256').update(bytes).digest('hex');
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function controllerErrorArtifact(error, requestId = null) {
|
|
91
|
-
const artifact = {
|
|
92
|
-
schema: 'lattice.scripted_adapter_error.v1',
|
|
93
|
-
code: error?.code ?? 'SCRIPTED_CONTROLLER_FAILED',
|
|
94
|
-
message: String(error?.message ?? error),
|
|
95
|
-
request_id: typeof requestId === 'string' ? requestId : null,
|
|
96
|
-
detail: plain(error?.detail) ? structuredClone(error.detail) : {},
|
|
97
|
-
error_digest: '',
|
|
98
|
-
};
|
|
99
|
-
artifact.error_digest = selfDigest(artifact, 'error_digest');
|
|
100
|
-
return artifact;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function validateBootstrap(value) {
|
|
104
|
-
return exact(value, [
|
|
105
|
-
'schema',
|
|
106
|
-
'request_id',
|
|
107
|
-
'run_id',
|
|
108
|
-
'controller_socket_ref',
|
|
109
|
-
'supervisor_socket_ref',
|
|
110
|
-
'supervisor_session_nonce',
|
|
111
|
-
'bootstrap_digest',
|
|
112
|
-
])
|
|
113
|
-
&& value.schema === 'lattice.adapter_controller_bootstrap.v1'
|
|
114
|
-
&& ID.test(value.request_id ?? '')
|
|
115
|
-
&& ID.test(value.run_id ?? '')
|
|
116
|
-
&& typeof value.controller_socket_ref === 'string'
|
|
117
|
-
&& /^supervisor\/controllers\/[0-9A-Za-z][0-9A-Za-z._-]{0,127}\.sock$/u
|
|
118
|
-
.test(value.controller_socket_ref)
|
|
119
|
-
&& value.supervisor_socket_ref === 'supervisor/control.sock'
|
|
120
|
-
&& typeof value.supervisor_session_nonce === 'string'
|
|
121
|
-
&& value.supervisor_session_nonce.length >= 32
|
|
122
|
-
&& SHA256.test(value.bootstrap_digest ?? '')
|
|
123
|
-
&& selfDigest(value, 'bootstrap_digest') === value.bootstrap_digest;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function validateHandshakeRequest(value, bootstrap) {
|
|
127
|
-
return exact(value, [
|
|
128
|
-
'schema',
|
|
129
|
-
'request_id',
|
|
130
|
-
'run_id',
|
|
131
|
-
'supervisor_session_nonce',
|
|
132
|
-
'controller_socket_ref',
|
|
133
|
-
'challenge',
|
|
134
|
-
'request_digest',
|
|
135
|
-
])
|
|
136
|
-
&& value.schema === 'lattice.adapter_controller_handshake_request.v1'
|
|
137
|
-
&& ID.test(value.request_id ?? '')
|
|
138
|
-
&& value.run_id === bootstrap.run_id
|
|
139
|
-
&& value.supervisor_session_nonce === bootstrap.supervisor_session_nonce
|
|
140
|
-
&& value.controller_socket_ref === bootstrap.controller_socket_ref
|
|
141
|
-
&& typeof value.challenge === 'string'
|
|
142
|
-
&& value.challenge.length >= 32
|
|
143
|
-
&& SHA256.test(value.request_digest ?? '')
|
|
144
|
-
&& selfDigest(value, 'request_digest') === value.request_digest;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function safeWritePath(value) {
|
|
148
|
-
return typeof value === 'string'
|
|
149
|
-
&& value.length > 0
|
|
150
|
-
&& value === path.posix.normalize(value)
|
|
151
|
-
&& !path.posix.isAbsolute(value)
|
|
152
|
-
&& value !== '..'
|
|
153
|
-
&& !value.startsWith('../')
|
|
154
|
-
&& !value.includes('\0')
|
|
155
|
-
&& !['.git', '.lattice'].includes(value.split('/')[0]);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async function durableReplaceBytes(filePath, bytes, mode = 0o600) {
|
|
159
|
-
const directory = path.dirname(filePath);
|
|
160
|
-
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
161
|
-
const temporaryPath = path.join(directory, `.${path.basename(filePath)}-${process.pid}.tmp`);
|
|
162
|
-
let handle;
|
|
163
|
-
try {
|
|
164
|
-
handle = await open(
|
|
165
|
-
temporaryPath,
|
|
166
|
-
fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY,
|
|
167
|
-
mode,
|
|
168
|
-
);
|
|
169
|
-
await handle.writeFile(bytes);
|
|
170
|
-
await handle.sync();
|
|
171
|
-
await handle.close();
|
|
172
|
-
handle = null;
|
|
173
|
-
await rename(temporaryPath, filePath);
|
|
174
|
-
await chmod(filePath, mode);
|
|
175
|
-
const directoryHandle = await open(directory, fsConstants.O_RDONLY);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
await directoryHandle.
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
||
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (
|
|
385
|
-
fail('SCRIPTED_BOOTSTRAP_INVALID',
|
|
386
|
-
}
|
|
387
|
-
const
|
|
388
|
-
if (!
|
|
389
|
-
fail('SCRIPTED_BOOTSTRAP_INVALID', '
|
|
390
|
-
}
|
|
391
|
-
const
|
|
392
|
-
if (!
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
const
|
|
473
|
-
if (
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
const
|
|
517
|
-
const
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
*
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
);
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|| writeLease.
|
|
680
|
-
|| writeLease.
|
|
681
|
-
|| writeLease.
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
task.
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
fail('
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
971
|
-
for (const
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
const
|
|
1024
|
-
if (
|
|
1025
|
-
fail('SCRIPTED_BOOTSTRAP_INVALID', 'bootstrap
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
}
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
socket
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1
|
+
import net from 'node:net';
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
3
|
+
import { execFile, spawn } from 'node:child_process';
|
|
4
|
+
import { constants as fsConstants, readFileSync } from 'node:fs';
|
|
5
|
+
import {
|
|
6
|
+
chmod,
|
|
7
|
+
lstat,
|
|
8
|
+
mkdir,
|
|
9
|
+
open,
|
|
10
|
+
readFile,
|
|
11
|
+
realpath,
|
|
12
|
+
rename,
|
|
13
|
+
rm,
|
|
14
|
+
} from 'node:fs/promises';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { promisify } from 'node:util';
|
|
18
|
+
|
|
19
|
+
import { canonicalizeArtifact, digestArtifact } from './artifact-contracts.mjs';
|
|
20
|
+
import {
|
|
21
|
+
armStagedWriteLease,
|
|
22
|
+
CONTROLLER_OPERATIONS,
|
|
23
|
+
validateArmedWriteLease,
|
|
24
|
+
validateControllerRequest,
|
|
25
|
+
validateRuntimeAdapterCapabilities,
|
|
26
|
+
validateRuntimeHeartbeatPolicy,
|
|
27
|
+
} from './runtime-controller-protocol.mjs';
|
|
28
|
+
import {
|
|
29
|
+
selfDigest,
|
|
30
|
+
validateExecutorPacket,
|
|
31
|
+
validateExecutorReceipt,
|
|
32
|
+
} from './runtime-contracts.mjs';
|
|
33
|
+
import { captureWorktreeDiff } from './runtime-diff-observer.mjs';
|
|
34
|
+
import { validateSupervisorWriteGate } from './runtime-gate-store.mjs';
|
|
35
|
+
import { observeManagedProcessStartIdentity } from './runtime-managed-supervisor.mjs';
|
|
36
|
+
import { scriptedWorktreeId, scriptedWorktreePath } from './runtime-scripted-worktree.mjs';
|
|
37
|
+
|
|
38
|
+
const MAX_DOCUMENT_BYTES = 8_388_608;
|
|
39
|
+
const SHA256 = /^[0-9a-f]{64}$/u;
|
|
40
|
+
const ID = /^[0-9A-Za-z](?:[0-9A-Za-z._-]{0,127})$/u;
|
|
41
|
+
const execFileAsync = promisify(execFile);
|
|
42
|
+
const REQUEST_SCHEMA_TO_OPERATION = Object.freeze(Object.fromEntries([
|
|
43
|
+
['lattice.adapter_dispatch_request.v1', 'dispatch'],
|
|
44
|
+
['lattice.adapter_observe_request.v1', 'observe'],
|
|
45
|
+
['lattice.adapter_running_inventory_request.v1', 'inventory'],
|
|
46
|
+
['lattice.adapter_barrier_request.v1', 'barrier'],
|
|
47
|
+
['lattice.adapter_rebind_request.v1', 'rebind'],
|
|
48
|
+
['lattice.adapter_prepare_request.v1', 'prepare'],
|
|
49
|
+
['lattice.adapter_activate_request.v1', 'activate'],
|
|
50
|
+
['lattice.adapter_release_request.v1', 'release'],
|
|
51
|
+
['lattice.adapter_revoke_request.v1', 'revoke'],
|
|
52
|
+
]));
|
|
53
|
+
|
|
54
|
+
export class ScriptedAdapterControllerError extends Error {
|
|
55
|
+
constructor(code, message, detail = {}) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = 'ScriptedAdapterControllerError';
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.detail = detail;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fail(code, message, detail) {
|
|
64
|
+
throw new ScriptedAdapterControllerError(code, message, detail);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function plain(value) {
|
|
68
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
69
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function exact(value, fields) {
|
|
73
|
+
if (!plain(value)) return false;
|
|
74
|
+
const actual = Object.keys(value).sort();
|
|
75
|
+
const expected = [...fields].sort();
|
|
76
|
+
return actual.length === expected.length
|
|
77
|
+
&& actual.every((field, index) => field === expected[index]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sign(value, field) {
|
|
81
|
+
value[field] = '';
|
|
82
|
+
value[field] = selfDigest(value, field);
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function sha256Bytes(bytes) {
|
|
87
|
+
return createHash('sha256').update(bytes).digest('hex');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function controllerErrorArtifact(error, requestId = null) {
|
|
91
|
+
const artifact = {
|
|
92
|
+
schema: 'lattice.scripted_adapter_error.v1',
|
|
93
|
+
code: error?.code ?? 'SCRIPTED_CONTROLLER_FAILED',
|
|
94
|
+
message: String(error?.message ?? error),
|
|
95
|
+
request_id: typeof requestId === 'string' ? requestId : null,
|
|
96
|
+
detail: plain(error?.detail) ? structuredClone(error.detail) : {},
|
|
97
|
+
error_digest: '',
|
|
98
|
+
};
|
|
99
|
+
artifact.error_digest = selfDigest(artifact, 'error_digest');
|
|
100
|
+
return artifact;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function validateBootstrap(value) {
|
|
104
|
+
return exact(value, [
|
|
105
|
+
'schema',
|
|
106
|
+
'request_id',
|
|
107
|
+
'run_id',
|
|
108
|
+
'controller_socket_ref',
|
|
109
|
+
'supervisor_socket_ref',
|
|
110
|
+
'supervisor_session_nonce',
|
|
111
|
+
'bootstrap_digest',
|
|
112
|
+
])
|
|
113
|
+
&& value.schema === 'lattice.adapter_controller_bootstrap.v1'
|
|
114
|
+
&& ID.test(value.request_id ?? '')
|
|
115
|
+
&& ID.test(value.run_id ?? '')
|
|
116
|
+
&& typeof value.controller_socket_ref === 'string'
|
|
117
|
+
&& /^supervisor\/controllers\/[0-9A-Za-z][0-9A-Za-z._-]{0,127}\.sock$/u
|
|
118
|
+
.test(value.controller_socket_ref)
|
|
119
|
+
&& value.supervisor_socket_ref === 'supervisor/control.sock'
|
|
120
|
+
&& typeof value.supervisor_session_nonce === 'string'
|
|
121
|
+
&& value.supervisor_session_nonce.length >= 32
|
|
122
|
+
&& SHA256.test(value.bootstrap_digest ?? '')
|
|
123
|
+
&& selfDigest(value, 'bootstrap_digest') === value.bootstrap_digest;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function validateHandshakeRequest(value, bootstrap) {
|
|
127
|
+
return exact(value, [
|
|
128
|
+
'schema',
|
|
129
|
+
'request_id',
|
|
130
|
+
'run_id',
|
|
131
|
+
'supervisor_session_nonce',
|
|
132
|
+
'controller_socket_ref',
|
|
133
|
+
'challenge',
|
|
134
|
+
'request_digest',
|
|
135
|
+
])
|
|
136
|
+
&& value.schema === 'lattice.adapter_controller_handshake_request.v1'
|
|
137
|
+
&& ID.test(value.request_id ?? '')
|
|
138
|
+
&& value.run_id === bootstrap.run_id
|
|
139
|
+
&& value.supervisor_session_nonce === bootstrap.supervisor_session_nonce
|
|
140
|
+
&& value.controller_socket_ref === bootstrap.controller_socket_ref
|
|
141
|
+
&& typeof value.challenge === 'string'
|
|
142
|
+
&& value.challenge.length >= 32
|
|
143
|
+
&& SHA256.test(value.request_digest ?? '')
|
|
144
|
+
&& selfDigest(value, 'request_digest') === value.request_digest;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function safeWritePath(value) {
|
|
148
|
+
return typeof value === 'string'
|
|
149
|
+
&& value.length > 0
|
|
150
|
+
&& value === path.posix.normalize(value)
|
|
151
|
+
&& !path.posix.isAbsolute(value)
|
|
152
|
+
&& value !== '..'
|
|
153
|
+
&& !value.startsWith('../')
|
|
154
|
+
&& !value.includes('\0')
|
|
155
|
+
&& !['.git', '.lattice'].includes(value.split('/')[0]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function durableReplaceBytes(filePath, bytes, mode = 0o600) {
|
|
159
|
+
const directory = path.dirname(filePath);
|
|
160
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
161
|
+
const temporaryPath = path.join(directory, `.${path.basename(filePath)}-${process.pid}.tmp`);
|
|
162
|
+
let handle;
|
|
163
|
+
try {
|
|
164
|
+
handle = await open(
|
|
165
|
+
temporaryPath,
|
|
166
|
+
fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY,
|
|
167
|
+
mode,
|
|
168
|
+
);
|
|
169
|
+
await handle.writeFile(bytes);
|
|
170
|
+
await handle.sync();
|
|
171
|
+
await handle.close();
|
|
172
|
+
handle = null;
|
|
173
|
+
await rename(temporaryPath, filePath);
|
|
174
|
+
await chmod(filePath, mode);
|
|
175
|
+
const directoryHandle = await open(directory, fsConstants.O_RDONLY);
|
|
176
|
+
// Windowsはdirectory handleのfsyncを許さず常にEPERM/EINVALを返す(Node仕様)。
|
|
177
|
+
// win32のこの2値だけ許容し、他OS・他エラーは従来どおり失敗させる。
|
|
178
|
+
try {
|
|
179
|
+
await directoryHandle.sync();
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (process.platform !== 'win32' || !['EPERM', 'EINVAL'].includes(error?.code)) throw error;
|
|
182
|
+
} finally {
|
|
183
|
+
await directoryHandle.close();
|
|
184
|
+
}
|
|
185
|
+
} finally {
|
|
186
|
+
await handle?.close().catch(() => {});
|
|
187
|
+
await rm(temporaryPath, { force: true }).catch(() => {});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function requireSafeTarget(repoRoot, relativePath) {
|
|
192
|
+
if (!safeWritePath(relativePath)) {
|
|
193
|
+
fail('SCRIPTED_PACKET_REJECTED', 'write pathが安全なrepo-relative pathではない', {
|
|
194
|
+
path: relativePath,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const target = path.join(repoRoot, ...relativePath.split('/'));
|
|
198
|
+
if (!target.startsWith(`${repoRoot}${path.sep}`)) {
|
|
199
|
+
fail('SCRIPTED_PACKET_REJECTED', 'write pathがrepo外を指す', { path: relativePath });
|
|
200
|
+
}
|
|
201
|
+
let cursor = repoRoot;
|
|
202
|
+
for (const segment of relativePath.split('/').slice(0, -1)) {
|
|
203
|
+
cursor = path.join(cursor, segment);
|
|
204
|
+
try {
|
|
205
|
+
const info = await lstat(cursor);
|
|
206
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
207
|
+
fail('SCRIPTED_PACKET_REJECTED', 'write path祖先がreal directoryではない', {
|
|
208
|
+
path: relativePath,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
217
|
+
const parentReal = await realpath(path.dirname(target));
|
|
218
|
+
if (!parentReal.startsWith(`${repoRoot}${path.sep}`) && parentReal !== repoRoot) {
|
|
219
|
+
fail('SCRIPTED_PACKET_REJECTED', 'write path親がrepo外へ解決された', { path: relativePath });
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const info = await lstat(target);
|
|
223
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
224
|
+
fail('SCRIPTED_PACKET_REJECTED', 'write targetがregular fileではない', {
|
|
225
|
+
path: relativePath,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
} catch (error) {
|
|
229
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
230
|
+
}
|
|
231
|
+
return target;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function basePathExists(repoRoot, baseSha, relativePath) {
|
|
235
|
+
try {
|
|
236
|
+
await execFileAsync('git', ['cat-file', '-e', `${baseSha}:${relativePath}`], {
|
|
237
|
+
cwd: repoRoot,
|
|
238
|
+
encoding: 'utf8',
|
|
239
|
+
});
|
|
240
|
+
return true;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (error?.code === 128) return false;
|
|
243
|
+
fail('SCRIPTED_EXECUTION_FAILED', 'base treeのwrite pathを観測できない', {
|
|
244
|
+
path: relativePath,
|
|
245
|
+
message: String(error?.message ?? error),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function deterministicWriteBytes(packet, relativePath) {
|
|
251
|
+
return Buffer.from(`${canonicalizeArtifact({
|
|
252
|
+
schema: 'lattice.scripted_adapter_write.v1',
|
|
253
|
+
todo_id: packet.todo_id,
|
|
254
|
+
path: relativePath,
|
|
255
|
+
packet_digest: packet.packet_digest,
|
|
256
|
+
context_content_digest: packet.context_content_digest,
|
|
257
|
+
})}\n`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* packetの宣言writeを実行する。**worker processから呼ばれる。**
|
|
262
|
+
*
|
|
263
|
+
* controllerと同じprocessで走らせていた頃、このrunには走行中のTODOが存在せず、
|
|
264
|
+
* 実行時の観測も静止の証明も原理的に成立しなかった(ADR 0143 Decision 7・9)。
|
|
265
|
+
*/
|
|
266
|
+
export async function executePacket({ packet, repoRoot, extraWrites = [] }) {
|
|
267
|
+
const writes = packet.scope?.writes;
|
|
268
|
+
if (!Array.isArray(writes) || writes.length === 0
|
|
269
|
+
|| writes.some((entry, index) => !safeWritePath(entry)
|
|
270
|
+
|| (index > 0 && writes[index - 1] >= entry))) {
|
|
271
|
+
fail('SCRIPTED_PACKET_REJECTED', 'scope.writesは非空の昇順一意pathでなければならない');
|
|
272
|
+
}
|
|
273
|
+
// 宣言scope外への書き込みは、検知そのものを検証するために要る。writeがすべて宣言内に
|
|
274
|
+
// 収まる限り、実行時competitionは原理的に一度も起きないので、検知経路を実runで通せない。
|
|
275
|
+
// 宣言と実writeが食い違う状態を意図して作れることが、この面の受入条件である。
|
|
276
|
+
const allWrites = [...new Set([...writes, ...extraWrites])].sort();
|
|
277
|
+
const prepared = [];
|
|
278
|
+
for (const relativePath of allWrites) {
|
|
279
|
+
const target = await requireSafeTarget(repoRoot, relativePath);
|
|
280
|
+
const existedAtBase = await basePathExists(repoRoot, packet.base_sha, relativePath);
|
|
281
|
+
prepared.push({
|
|
282
|
+
relativePath,
|
|
283
|
+
target,
|
|
284
|
+
bytes: deterministicWriteBytes(packet, relativePath),
|
|
285
|
+
change: existedAtBase ? 'modified' : 'added',
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
for (const entry of prepared) {
|
|
289
|
+
await durableReplaceBytes(entry.target, entry.bytes);
|
|
290
|
+
const observed = await readFile(entry.target);
|
|
291
|
+
if (!observed.equals(entry.bytes)) {
|
|
292
|
+
fail('SCRIPTED_EXECUTION_FAILED', 'write後bytesが決定的内容と一致しない', {
|
|
293
|
+
path: entry.relativePath,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const observedDiff = prepared.map((entry) => ({
|
|
298
|
+
path: entry.relativePath,
|
|
299
|
+
change: entry.change,
|
|
300
|
+
}));
|
|
301
|
+
const checkpointDigest = digestArtifact({
|
|
302
|
+
schema: 'lattice.scripted_adapter_checkpoint.v1',
|
|
303
|
+
packet_digest: packet.packet_digest,
|
|
304
|
+
observed_diff: observedDiff,
|
|
305
|
+
content_digests: prepared.map((entry) => ({
|
|
306
|
+
path: entry.relativePath,
|
|
307
|
+
digest: sha256Bytes(entry.bytes),
|
|
308
|
+
})),
|
|
309
|
+
});
|
|
310
|
+
return { observedDiff, checkpointDigest };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function readAndValidateGate(runDir, writeLease) {
|
|
314
|
+
const gatePath = path.join(runDir, 'supervisor', 'write-gate.json');
|
|
315
|
+
let gate;
|
|
316
|
+
try {
|
|
317
|
+
gate = JSON.parse(await readFile(gatePath, 'utf8'));
|
|
318
|
+
} catch (error) {
|
|
319
|
+
fail('SCRIPTED_WRITE_GATE_REJECTED', '中央write gateを読めない', {
|
|
320
|
+
message: String(error?.message ?? error),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
if (!validateSupervisorWriteGate(gate)
|
|
324
|
+
|| gate.run_id !== writeLease.run_id
|
|
325
|
+
|| gate.plan_epoch !== writeLease.plan_epoch
|
|
326
|
+
|| gate.gate_generation !== writeLease.gate_generation
|
|
327
|
+
|| gate.release_barrier_digest !== writeLease.release_barrier_digest
|
|
328
|
+
|| !gate.armed_lease_digests.includes(writeLease.lease_digest)) {
|
|
329
|
+
fail('SCRIPTED_WRITE_GATE_REJECTED', '中央write gateがarmed leaseを認可しない');
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* supervisor controller protocolを実装する決定論的scripted adapter。
|
|
335
|
+
* 時刻はheartbeat schedulingだけに使い、write bytes/handle/receiptへ混入させない。
|
|
336
|
+
*/
|
|
337
|
+
/**
|
|
338
|
+
* 登録済みadapter configから、このcontrollerの振る舞いを読む。
|
|
339
|
+
*
|
|
340
|
+
* **configはregistrationのdigestへ束縛されている。** repo内の任意のfileを信用するのではなく、
|
|
341
|
+
* `run adapter register`が記録した`config_digest`と一致するbytesだけを受ける。一致しなければ
|
|
342
|
+
* 既定の振る舞いへ落とすのではなく止める——registrationと実configがずれた状態で走ると、
|
|
343
|
+
* 記録されたrunの再現性が壊れる。
|
|
344
|
+
*
|
|
345
|
+
* 読むのは3つだけ:
|
|
346
|
+
* - `hold_ms`: 書き込み後にworkerが走り続ける時間。実行時観測が成立する窓を作る。
|
|
347
|
+
* - `extra_writes`: 全TODOに共通する宣言scope外write。
|
|
348
|
+
* - `extra_writes_by_todo`: 競合当事者だけへscope外writeを与える実daemon fixture用設定。
|
|
349
|
+
* - `mode`: 既存の`deterministic`のみ。未知の値は黙って無視せず止める。
|
|
350
|
+
*/
|
|
351
|
+
async function readScriptedBehavior(repoRoot) {
|
|
352
|
+
const descriptorPath = path.join(repoRoot, '.lattice', 'runtime', 'adapter-registry',
|
|
353
|
+
'descriptors', 'scripted.json');
|
|
354
|
+
let descriptor;
|
|
355
|
+
try {
|
|
356
|
+
descriptor = JSON.parse(await readFile(descriptorPath, 'utf8'));
|
|
357
|
+
} catch {
|
|
358
|
+
// 未登録のまま走らせる経路(unit test等)は既定の振る舞いで動かす。
|
|
359
|
+
return { hold_ms: 0, extra_writes: [], extra_writes_by_todo: {} };
|
|
360
|
+
}
|
|
361
|
+
if (typeof descriptor?.config_ref !== 'string' || typeof descriptor.config_digest !== 'string') {
|
|
362
|
+
return { hold_ms: 0, extra_writes: [], extra_writes_by_todo: {} };
|
|
363
|
+
}
|
|
364
|
+
const configPath = path.join(repoRoot, ...descriptor.config_ref.split('/'));
|
|
365
|
+
let bytes;
|
|
366
|
+
try {
|
|
367
|
+
bytes = await readFile(configPath);
|
|
368
|
+
} catch {
|
|
369
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', '登録済みadapter configを読めない', {
|
|
370
|
+
config_ref: descriptor.config_ref,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
if (sha256Bytes(bytes) !== descriptor.config_digest) {
|
|
374
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'adapter configが登録時のdigestと一致しない', {
|
|
375
|
+
config_ref: descriptor.config_ref,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
let config;
|
|
379
|
+
try {
|
|
380
|
+
config = JSON.parse(bytes.toString('utf8'));
|
|
381
|
+
} catch {
|
|
382
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'adapter configのJSONが不正');
|
|
383
|
+
}
|
|
384
|
+
if (config?.mode !== undefined && config.mode !== 'deterministic') {
|
|
385
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', `未知のscripted mode: ${String(config.mode)}`);
|
|
386
|
+
}
|
|
387
|
+
const holdMs = config?.hold_ms ?? 0;
|
|
388
|
+
if (!Number.isSafeInteger(holdMs) || holdMs < 0 || holdMs > 600_000) {
|
|
389
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'hold_msが不正');
|
|
390
|
+
}
|
|
391
|
+
const extraWrites = config?.extra_writes ?? [];
|
|
392
|
+
if (!Array.isArray(extraWrites) || extraWrites.some((entry) => !safeWritePath(entry))) {
|
|
393
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'extra_writesが不正');
|
|
394
|
+
}
|
|
395
|
+
const extraWritesByTodo = config?.extra_writes_by_todo ?? {};
|
|
396
|
+
if (!plain(extraWritesByTodo) || Object.entries(extraWritesByTodo).some(([todoId, entries]) => (
|
|
397
|
+
!ID.test(todoId) || !Array.isArray(entries) || entries.some((entry) => !safeWritePath(entry))
|
|
398
|
+
))) {
|
|
399
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'extra_writes_by_todoが不正');
|
|
400
|
+
}
|
|
401
|
+
return { hold_ms: holdMs, extra_writes: [...extraWrites],
|
|
402
|
+
extra_writes_by_todo: Object.fromEntries(Object.entries(extraWritesByTodo)
|
|
403
|
+
.map(([todoId, entries]) => [todoId, [...entries]])) };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const WORKER_ENTRYPOINT = fileURLToPath(new URL('../bin/lattice-scripted-worker.mjs', import.meta.url));
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* worker processを起こす(ADR 0143 Decision 9)。
|
|
410
|
+
*
|
|
411
|
+
* `detached`で独立process groupへ置く。controllerと同じgroupに居ると、直接OS観測が
|
|
412
|
+
* 「未記録process group memberを検出」として落とす——正しく落ちるので、ここを外さない。
|
|
413
|
+
*
|
|
414
|
+
* 起動直後にprocess start identityを観測して束ねる。pidは再利用されうるので、
|
|
415
|
+
* pidだけでは「同じprocessか」を後から言えない。
|
|
416
|
+
*/
|
|
417
|
+
async function spawnScriptedWorker({ packet, worktreePath, extraWrites, holdMs,
|
|
418
|
+
controllerId, runDir }) {
|
|
419
|
+
const job = {
|
|
420
|
+
schema: 'lattice.scripted_worker_job.v1',
|
|
421
|
+
packet: structuredClone(packet),
|
|
422
|
+
worktree_path: worktreePath,
|
|
423
|
+
extra_writes: [...extraWrites],
|
|
424
|
+
hold_ms: holdMs,
|
|
425
|
+
};
|
|
426
|
+
const jobPath = path.join(runDir, 'controllers', controllerId, 'jobs',
|
|
427
|
+
`${packet.packet_digest}.json`);
|
|
428
|
+
await durableReplaceBytes(jobPath, Buffer.from(`${canonicalizeArtifact(job)}\n`));
|
|
429
|
+
const child = spawn(process.execPath, [WORKER_ENTRYPOINT, jobPath], {
|
|
430
|
+
detached: true, stdio: ['ignore', 'pipe', 'pipe'], cwd: worktreePath,
|
|
431
|
+
});
|
|
432
|
+
if (!Number.isSafeInteger(child.pid)) {
|
|
433
|
+
fail('SCRIPTED_EXECUTION_FAILED', 'worker processを起こせない');
|
|
434
|
+
}
|
|
435
|
+
const identity = await observeManagedProcessStartIdentity(child.pid);
|
|
436
|
+
let stdout = '';
|
|
437
|
+
let stderr = '';
|
|
438
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
|
|
439
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
|
|
440
|
+
const completed = new Promise((resolve, reject) => {
|
|
441
|
+
child.once('error', reject);
|
|
442
|
+
child.once('close', (code, signal) => {
|
|
443
|
+
const line = stdout.split('\n').find((entry) => entry.includes('scripted_worker_result'));
|
|
444
|
+
let parsed = null;
|
|
445
|
+
try { parsed = line === undefined ? null : JSON.parse(line); } catch { parsed = null; }
|
|
446
|
+
if (parsed?.schema !== 'lattice.scripted_worker_result.v1') {
|
|
447
|
+
reject(new TypeError(`worker結果を読めない (${signal ?? code}): ${stderr.trim() || '(出力なし)'}`));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
resolve({ observedDiff: parsed.observed_diff, checkpointDigest: parsed.checkpoint_digest });
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
return {
|
|
454
|
+
pid: child.pid,
|
|
455
|
+
process_group_id: child.pid,
|
|
456
|
+
process_start_identity: identity,
|
|
457
|
+
child,
|
|
458
|
+
completed,
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export async function createScriptedAdapterController({
|
|
463
|
+
bootstrap,
|
|
464
|
+
runDir,
|
|
465
|
+
repoRoot,
|
|
466
|
+
controllerSessionNonce = randomBytes(32).toString('hex'),
|
|
467
|
+
}) {
|
|
468
|
+
if (!validateBootstrap(bootstrap)) {
|
|
469
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'controller bootstrapが不正');
|
|
470
|
+
}
|
|
471
|
+
const canonicalRunDir = await realpath(runDir);
|
|
472
|
+
const canonicalRepoRoot = await realpath(repoRoot);
|
|
473
|
+
if (path.resolve(canonicalRunDir, '..', '..', '..') !== canonicalRepoRoot) {
|
|
474
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'run storeとrepo rootのbindingが不正');
|
|
475
|
+
}
|
|
476
|
+
const controllerId = path.basename(bootstrap.controller_socket_ref, '.sock');
|
|
477
|
+
if (!controllerId.startsWith('scripted-')
|
|
478
|
+
|| typeof controllerSessionNonce !== 'string'
|
|
479
|
+
|| controllerSessionNonce.length < 32) {
|
|
480
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'scripted controller identityが不正');
|
|
481
|
+
}
|
|
482
|
+
const capabilities = sign({
|
|
483
|
+
schema: 'lattice.runtime_adapter_capabilities.v1',
|
|
484
|
+
operations: [...CONTROLLER_OPERATIONS],
|
|
485
|
+
process_observation: true,
|
|
486
|
+
worktree_fingerprint: true,
|
|
487
|
+
staged_write_lease: true,
|
|
488
|
+
durable_dispatch: true,
|
|
489
|
+
capabilities_digest: '',
|
|
490
|
+
}, 'capabilities_digest');
|
|
491
|
+
if (!validateRuntimeAdapterCapabilities(capabilities)) {
|
|
492
|
+
fail('SCRIPTED_CONTROLLER_INVALID', 'capabilities生成に失敗した');
|
|
493
|
+
}
|
|
494
|
+
const heartbeat = sign({
|
|
495
|
+
schema: 'lattice.runtime_heartbeat_policy.v1',
|
|
496
|
+
interval_ms: 1_000,
|
|
497
|
+
ttl_ms: 10_000,
|
|
498
|
+
disconnect_revokes_immediately: true,
|
|
499
|
+
policy_digest: '',
|
|
500
|
+
}, 'policy_digest');
|
|
501
|
+
if (!validateRuntimeHeartbeatPolicy(heartbeat)) {
|
|
502
|
+
fail('SCRIPTED_CONTROLLER_INVALID', 'heartbeat policy生成に失敗した');
|
|
503
|
+
}
|
|
504
|
+
const descriptor = sign({
|
|
505
|
+
schema: 'lattice.runtime_adapter_controller_descriptor.v1',
|
|
506
|
+
controller_id: controllerId,
|
|
507
|
+
adapter_kind: 'scripted',
|
|
508
|
+
pid: process.pid,
|
|
509
|
+
process_start_identity: await observeManagedProcessStartIdentity(process.pid),
|
|
510
|
+
socket_ref: bootstrap.controller_socket_ref,
|
|
511
|
+
controller_session_nonce_digest: digestArtifact(controllerSessionNonce),
|
|
512
|
+
capabilities,
|
|
513
|
+
heartbeat,
|
|
514
|
+
descriptor_digest: '',
|
|
515
|
+
}, 'descriptor_digest');
|
|
516
|
+
const sessionNonceDigest = digestArtifact(bootstrap.supervisor_session_nonce);
|
|
517
|
+
const behavior = await readScriptedBehavior(canonicalRepoRoot);
|
|
518
|
+
const stagedLeases = new Map();
|
|
519
|
+
const armedLeases = new Map();
|
|
520
|
+
const preparedPackets = new Map();
|
|
521
|
+
const tasks = new Map();
|
|
522
|
+
const todoToHandle = new Map();
|
|
523
|
+
let currentEpoch = 1;
|
|
524
|
+
let registrationDigest = null;
|
|
525
|
+
|
|
526
|
+
/** dispatch応答が運ぶworker process。誰を止めればよいかをsupervisorへ名指しする。 */
|
|
527
|
+
/**
|
|
528
|
+
* 起こしたworker processを畳む。
|
|
529
|
+
*
|
|
530
|
+
* **SIGKILLでなければ届かない。** barrierで止めたworkerはSIGSTOP状態にあり、
|
|
531
|
+
* SIGTERMは継続されるまで配送されない。放置すると、runもcontrollerもrepoも消えた後まで
|
|
532
|
+
* 停止したままのprocessが残り続ける(自分では終われない)。
|
|
533
|
+
*/
|
|
534
|
+
const reapWorkers = () => {
|
|
535
|
+
for (const task of tasks.values()) {
|
|
536
|
+
const pid = task.worker?.pid;
|
|
537
|
+
if (!Number.isSafeInteger(pid)) continue;
|
|
538
|
+
try { process.kill(pid, 'SIGKILL'); } catch { /* 既に終わっている */ }
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const workerProcessOf = (task) => ({
|
|
543
|
+
pid: task.worker.pid,
|
|
544
|
+
process_group_id: task.worker.process_group_id,
|
|
545
|
+
process_start_identity: structuredClone(task.worker.process_start_identity),
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
const persistReceipt = async (receipt) => {
|
|
549
|
+
const payloadDigest = digestArtifact(receipt);
|
|
550
|
+
const receiptPath = path.join(
|
|
551
|
+
canonicalRunDir,
|
|
552
|
+
'controllers',
|
|
553
|
+
controllerId,
|
|
554
|
+
'receipts',
|
|
555
|
+
`${payloadDigest}.json`,
|
|
556
|
+
);
|
|
557
|
+
await durableReplaceBytes(
|
|
558
|
+
receiptPath,
|
|
559
|
+
Buffer.from(`${canonicalizeArtifact(receipt)}\n`),
|
|
560
|
+
);
|
|
561
|
+
return payloadDigest;
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const responseFor = async (operation, request) => {
|
|
565
|
+
if (!validateControllerRequest(operation, request)) {
|
|
566
|
+
fail('SCRIPTED_REQUEST_INVALID', `${operation} requestがprotocol contractを満たさない`);
|
|
567
|
+
}
|
|
568
|
+
if (registrationDigest !== null && request.registration_digest !== registrationDigest) {
|
|
569
|
+
fail('SCRIPTED_REGISTRATION_MISMATCH', 'controller registration digestが変化した');
|
|
570
|
+
}
|
|
571
|
+
registrationDigest = request.registration_digest;
|
|
572
|
+
if (operation === 'prepare') {
|
|
573
|
+
const packet = request.executor_packet;
|
|
574
|
+
const lease = request.staged_lease;
|
|
575
|
+
if (lease.run_id !== bootstrap.run_id || lease.todo_id !== packet.todo_id
|
|
576
|
+
|| lease.plan_epoch !== packet.plan_epoch || lease.packet_digest !== packet.packet_digest
|
|
577
|
+
|| lease.controller_registration_digest !== request.registration_digest
|
|
578
|
+
|| lease.supervisor_session_nonce_digest !== sessionNonceDigest) {
|
|
579
|
+
fail('SCRIPTED_LEASE_REJECTED', 'prepare packetとstaged leaseのbindingが不正');
|
|
580
|
+
}
|
|
581
|
+
const prior = stagedLeases.get(lease.lease_digest);
|
|
582
|
+
if (prior !== undefined
|
|
583
|
+
&& canonicalizeArtifact(prior) !== canonicalizeArtifact(lease)) {
|
|
584
|
+
fail('SCRIPTED_LEASE_REJECTED', '同じdigestのstaged lease bytesが変化した');
|
|
585
|
+
}
|
|
586
|
+
stagedLeases.set(lease.lease_digest, structuredClone(lease));
|
|
587
|
+
preparedPackets.set(packet.packet_digest, structuredClone(packet));
|
|
588
|
+
currentEpoch = packet.plan_epoch;
|
|
589
|
+
const ack = sign({
|
|
590
|
+
schema: 'lattice.adapter_prepare_ack.v1',
|
|
591
|
+
ack_id: `prepare-${packet.packet_digest.slice(0, 24)}`,
|
|
592
|
+
registration_digest: request.registration_digest,
|
|
593
|
+
controller_id: controllerId,
|
|
594
|
+
run_id: bootstrap.run_id,
|
|
595
|
+
todo_id: packet.todo_id,
|
|
596
|
+
plan_epoch: packet.plan_epoch,
|
|
597
|
+
packet_digest: packet.packet_digest,
|
|
598
|
+
staged_lease_digest: lease.lease_digest,
|
|
599
|
+
supervisor_session_nonce_digest: sessionNonceDigest,
|
|
600
|
+
ack_digest: '',
|
|
601
|
+
}, 'ack_digest');
|
|
602
|
+
return sign({
|
|
603
|
+
schema: 'lattice.adapter_prepare_response.v1',
|
|
604
|
+
request_id: request.request_id,
|
|
605
|
+
prepare_ack: ack,
|
|
606
|
+
staged_lease_digest: lease.lease_digest,
|
|
607
|
+
response_digest: '',
|
|
608
|
+
}, 'response_digest');
|
|
609
|
+
}
|
|
610
|
+
if (operation === 'activate') {
|
|
611
|
+
if (request.staged_lease_digests.some((digest) => !stagedLeases.has(digest))) {
|
|
612
|
+
fail('SCRIPTED_LEASE_REJECTED', 'activateが未知のstaged leaseを含む');
|
|
613
|
+
}
|
|
614
|
+
const epochs = new Set(request.staged_lease_digests
|
|
615
|
+
.map((digest) => stagedLeases.get(digest).plan_epoch));
|
|
616
|
+
if (epochs.size > 1) fail('SCRIPTED_LEASE_REJECTED', 'activate leaseのepochが混在する');
|
|
617
|
+
if (epochs.size === 1) currentEpoch = [...epochs][0];
|
|
618
|
+
const ack = sign({
|
|
619
|
+
schema: 'lattice.adapter_ready_ack.v1',
|
|
620
|
+
ack_id: `ready-${request.activation_digest.slice(0, 24)}`,
|
|
621
|
+
registration_digest: request.registration_digest,
|
|
622
|
+
controller_id: controllerId,
|
|
623
|
+
run_id: bootstrap.run_id,
|
|
624
|
+
plan_epoch: currentEpoch,
|
|
625
|
+
activation_digest: request.activation_digest,
|
|
626
|
+
staged_lease_digests: [...request.staged_lease_digests],
|
|
627
|
+
supervisor_session_nonce_digest: sessionNonceDigest,
|
|
628
|
+
ack_digest: '',
|
|
629
|
+
}, 'ack_digest');
|
|
630
|
+
return sign({
|
|
631
|
+
schema: 'lattice.adapter_activate_response.v1',
|
|
632
|
+
request_id: request.request_id,
|
|
633
|
+
ready_ack: ack,
|
|
634
|
+
observed_pointer_digest: request.committed_epoch_digest,
|
|
635
|
+
response_digest: '',
|
|
636
|
+
}, 'response_digest');
|
|
637
|
+
}
|
|
638
|
+
if (operation === 'release') {
|
|
639
|
+
const armed = [];
|
|
640
|
+
for (const stagedDigest of request.staged_lease_digests) {
|
|
641
|
+
const staged = stagedLeases.get(stagedDigest);
|
|
642
|
+
if (staged === undefined) {
|
|
643
|
+
fail('SCRIPTED_LEASE_REJECTED', 'releaseが未知のstaged leaseを含む');
|
|
644
|
+
}
|
|
645
|
+
const lease = armStagedWriteLease(staged, {
|
|
646
|
+
releaseBarrierDigest: request.release_barrier_digest,
|
|
647
|
+
gateGeneration: request.gate_generation,
|
|
648
|
+
});
|
|
649
|
+
stagedLeases.delete(stagedDigest);
|
|
650
|
+
armedLeases.set(lease.lease_digest, lease);
|
|
651
|
+
armed.push(lease.lease_digest);
|
|
652
|
+
}
|
|
653
|
+
armed.sort();
|
|
654
|
+
const ack = sign({
|
|
655
|
+
schema: 'lattice.adapter_release_ack.v1',
|
|
656
|
+
ack_id: `release-${request.release_barrier_digest.slice(0, 24)}`,
|
|
657
|
+
registration_digest: request.registration_digest,
|
|
658
|
+
controller_id: controllerId,
|
|
659
|
+
run_id: bootstrap.run_id,
|
|
660
|
+
plan_epoch: currentEpoch,
|
|
661
|
+
release_barrier_digest: request.release_barrier_digest,
|
|
662
|
+
gate_generation: request.gate_generation,
|
|
663
|
+
armed_lease_digests: armed,
|
|
664
|
+
supervisor_session_nonce_digest: sessionNonceDigest,
|
|
665
|
+
ack_digest: '',
|
|
666
|
+
}, 'ack_digest');
|
|
667
|
+
return sign({
|
|
668
|
+
schema: 'lattice.adapter_release_response.v1',
|
|
669
|
+
request_id: request.request_id,
|
|
670
|
+
release_ack: ack,
|
|
671
|
+
armed_lease_digests: armed,
|
|
672
|
+
observed_gate_generation: request.gate_generation,
|
|
673
|
+
response_digest: '',
|
|
674
|
+
}, 'response_digest');
|
|
675
|
+
}
|
|
676
|
+
if (operation === 'dispatch') {
|
|
677
|
+
const { packet, write_lease: writeLease } = request;
|
|
678
|
+
if (!validateExecutorPacket(packet) || !validateArmedWriteLease(writeLease)
|
|
679
|
+
|| !armedLeases.has(writeLease.lease_digest)
|
|
680
|
+
|| writeLease.run_id !== bootstrap.run_id
|
|
681
|
+
|| writeLease.todo_id !== packet.todo_id
|
|
682
|
+
|| writeLease.plan_epoch !== packet.plan_epoch
|
|
683
|
+
|| writeLease.packet_digest !== packet.packet_digest
|
|
684
|
+
|| writeLease.controller_registration_digest !== request.registration_digest
|
|
685
|
+
|| writeLease.supervisor_session_nonce_digest !== sessionNonceDigest) {
|
|
686
|
+
fail('SCRIPTED_PACKET_REJECTED', 'dispatch packetとarmed leaseのbindingが不正');
|
|
687
|
+
}
|
|
688
|
+
const priorHandle = todoToHandle.get(packet.todo_id);
|
|
689
|
+
if (priorHandle !== undefined) {
|
|
690
|
+
const prior = tasks.get(priorHandle);
|
|
691
|
+
if (prior.packet.packet_digest === packet.packet_digest
|
|
692
|
+
&& prior.lease.lease_digest === writeLease.lease_digest) {
|
|
693
|
+
return sign({
|
|
694
|
+
schema: 'lattice.adapter_dispatch_response.v2',
|
|
695
|
+
request_id: request.request_id,
|
|
696
|
+
executor_handle: prior.executorHandle,
|
|
697
|
+
worktree_id: prior.worktreeId,
|
|
698
|
+
packet_digest: packet.packet_digest,
|
|
699
|
+
lease_digest: writeLease.lease_digest,
|
|
700
|
+
worker_process: workerProcessOf(prior),
|
|
701
|
+
response_digest: '',
|
|
702
|
+
}, 'response_digest');
|
|
703
|
+
}
|
|
704
|
+
if (prior.state !== 'held') {
|
|
705
|
+
fail('SCRIPTED_DUPLICATE_DISPATCH', '同じTODOへ異なるpacketをdispatchできない');
|
|
706
|
+
}
|
|
707
|
+
// 対象限定holdで失効したattemptだけを畳み、同じTODOのsuccessorを起動する。
|
|
708
|
+
// closure外attemptはtodoToHandleを保持するので、この経路へ入らない。
|
|
709
|
+
try { process.kill(-prior.worker.process_group_id, 'SIGKILL'); }
|
|
710
|
+
catch (error) {
|
|
711
|
+
if (error?.code !== 'ESRCH') {
|
|
712
|
+
fail('SCRIPTED_EXECUTION_FAILED', '旧attemptを終了できない', {
|
|
713
|
+
pid: prior.worker.pid, reason: String(error?.code ?? error?.message ?? error),
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
prior.state = 'superseded';
|
|
718
|
+
todoToHandle.delete(packet.todo_id);
|
|
719
|
+
}
|
|
720
|
+
await readAndValidateGate(canonicalRunDir, writeLease);
|
|
721
|
+
// canonical repoではなく自分の木へ書く。共有rootでは、書き込みの帰属をrootから
|
|
722
|
+
// 決められないので早期警報もcheckpoint判定も成立しない。木はsupervisorが
|
|
723
|
+
// dispatch前に用意する(監視を張るのがdispatchより前でなければ観測を取り逃す)。
|
|
724
|
+
const worktreePath = scriptedWorktreePath({ runDir: canonicalRunDir, packet });
|
|
725
|
+
try {
|
|
726
|
+
const info = await lstat(worktreePath);
|
|
727
|
+
if (!info.isDirectory()) throw new TypeError('not a directory');
|
|
728
|
+
} catch {
|
|
729
|
+
fail('SCRIPTED_EXECUTION_FAILED', 'supervisorが用意したworktreeが無い', {
|
|
730
|
+
worktree_path: worktreePath,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
const executorHandle = `scripted-${packet.packet_digest.slice(0, 24)}`;
|
|
734
|
+
const worktreeId = scriptedWorktreeId(packet);
|
|
735
|
+
// **dispatchで作業を終わらせない。** 終わらせると、走行中のTODOが1つも存在しない
|
|
736
|
+
// runになり、実行時の観測——書き込みを見て、他のworkerとの重なりを掴む——が
|
|
737
|
+
// 原理的に成立しない。dispatchは作業を起こして返り、完了はobserveが拾う。
|
|
738
|
+
const progress = {
|
|
739
|
+
schema: 'lattice.scripted_adapter_progress.v1',
|
|
740
|
+
run_id: bootstrap.run_id,
|
|
741
|
+
todo_id: packet.todo_id,
|
|
742
|
+
executor_handle: executorHandle,
|
|
743
|
+
worktree_id: worktreeId,
|
|
744
|
+
state: 'running',
|
|
745
|
+
};
|
|
746
|
+
const worktreeReal = await realpath(worktreePath);
|
|
747
|
+
// **workerは別processで起こす。** controller自身のprocessで作業すると、holdが
|
|
748
|
+
// 要求する静止を証明できない——止めれば応答できず、止めなければ証明できない。
|
|
749
|
+
// `detached`で独立process groupへ置く。同じgroupにcontrollerが居ると、直接OS観測が
|
|
750
|
+
// 「未記録process group memberを検出」として正しく落とす。
|
|
751
|
+
const spawned = await spawnScriptedWorker({
|
|
752
|
+
packet, worktreePath: worktreeReal,
|
|
753
|
+
extraWrites: behavior.extra_writes_by_todo[packet.todo_id] ?? behavior.extra_writes,
|
|
754
|
+
holdMs: behavior.hold_ms, controllerId, runDir: canonicalRunDir,
|
|
755
|
+
});
|
|
756
|
+
const task = {
|
|
757
|
+
packet: structuredClone(packet),
|
|
758
|
+
lease: structuredClone(writeLease),
|
|
759
|
+
executorHandle,
|
|
760
|
+
worktreeId,
|
|
761
|
+
worktreePath: worktreeReal,
|
|
762
|
+
receipt: null,
|
|
763
|
+
payloadDigest: digestArtifact(progress),
|
|
764
|
+
state: 'running',
|
|
765
|
+
failure: null,
|
|
766
|
+
settled: null,
|
|
767
|
+
worker: spawned,
|
|
768
|
+
};
|
|
769
|
+
tasks.set(executorHandle, task);
|
|
770
|
+
todoToHandle.set(packet.todo_id, executorHandle);
|
|
771
|
+
task.settled = (async () => {
|
|
772
|
+
try {
|
|
773
|
+
const { observedDiff, checkpointDigest } = await spawned.completed;
|
|
774
|
+
const receipt = sign({
|
|
775
|
+
schema: 'lattice.executor_receipt.v1',
|
|
776
|
+
receipt_id: `receipt-${packet.packet_digest.slice(0, 24)}`,
|
|
777
|
+
executor_handle: executorHandle,
|
|
778
|
+
worktree_id: worktreeId,
|
|
779
|
+
base_sha: packet.base_sha,
|
|
780
|
+
plan_epoch: packet.plan_epoch,
|
|
781
|
+
packet_digest: packet.packet_digest,
|
|
782
|
+
todo_id: packet.todo_id,
|
|
783
|
+
checkpoint_digest: checkpointDigest,
|
|
784
|
+
observed_diff: observedDiff,
|
|
785
|
+
receipt_digest: '',
|
|
786
|
+
}, 'receipt_digest');
|
|
787
|
+
if (!validateExecutorReceipt(receipt)) {
|
|
788
|
+
throw new TypeError('生成receiptがexecutor contractを満たさない');
|
|
789
|
+
}
|
|
790
|
+
task.payloadDigest = await persistReceipt(receipt);
|
|
791
|
+
task.receipt = receipt;
|
|
792
|
+
task.state = 'terminal';
|
|
793
|
+
} catch (error) {
|
|
794
|
+
// 失敗を走行中のまま放置しない。observeがtypedに落ちる形へ残す。
|
|
795
|
+
task.failure = String(error?.detail?.reason ?? error?.message ?? error);
|
|
796
|
+
}
|
|
797
|
+
})();
|
|
798
|
+
return sign({
|
|
799
|
+
schema: 'lattice.adapter_dispatch_response.v2',
|
|
800
|
+
request_id: request.request_id,
|
|
801
|
+
executor_handle: executorHandle,
|
|
802
|
+
worktree_id: worktreeId,
|
|
803
|
+
packet_digest: packet.packet_digest,
|
|
804
|
+
lease_digest: writeLease.lease_digest,
|
|
805
|
+
worker_process: workerProcessOf(task),
|
|
806
|
+
response_digest: '',
|
|
807
|
+
}, 'response_digest');
|
|
808
|
+
}
|
|
809
|
+
if (operation === 'observe') {
|
|
810
|
+
const task = tasks.get(request.executor_handle);
|
|
811
|
+
if (task === undefined
|
|
812
|
+
|| task.packet.plan_epoch !== request.expected_epoch
|
|
813
|
+
|| task.lease.lease_digest !== request.expected_lease_digest) {
|
|
814
|
+
fail('SCRIPTED_OBSERVATION_REJECTED', 'observe bindingがdispatch記録と一致しない');
|
|
815
|
+
}
|
|
816
|
+
// 走行中に落ちた作業を「まだ走っている」と言い続けない。
|
|
817
|
+
if (task.failure !== null) {
|
|
818
|
+
fail('SCRIPTED_EXECUTION_FAILED', 'worker実行が失敗した', { reason: task.failure });
|
|
819
|
+
}
|
|
820
|
+
const observation = sign({
|
|
821
|
+
schema: 'lattice.adapter_observation.v1',
|
|
822
|
+
state: task.state,
|
|
823
|
+
executor_handle: task.executorHandle,
|
|
824
|
+
plan_epoch: task.packet.plan_epoch,
|
|
825
|
+
lease_digest: task.lease.lease_digest,
|
|
826
|
+
payload_digest: task.payloadDigest,
|
|
827
|
+
observation_digest: '',
|
|
828
|
+
}, 'observation_digest');
|
|
829
|
+
return sign({
|
|
830
|
+
schema: 'lattice.adapter_observe_response.v1',
|
|
831
|
+
request_id: request.request_id,
|
|
832
|
+
observation,
|
|
833
|
+
observation_digest: observation.observation_digest,
|
|
834
|
+
response_digest: '',
|
|
835
|
+
}, 'response_digest');
|
|
836
|
+
}
|
|
837
|
+
if (operation === 'inventory') {
|
|
838
|
+
const runningBindings = [...tasks.values()]
|
|
839
|
+
.filter((task) => task.state === 'running')
|
|
840
|
+
.map((task) => ({
|
|
841
|
+
todo_id: task.packet.todo_id,
|
|
842
|
+
executor_handle: task.executorHandle,
|
|
843
|
+
worktree_id: task.worktreeId,
|
|
844
|
+
plan_epoch: task.packet.plan_epoch,
|
|
845
|
+
packet_digest: task.packet.packet_digest,
|
|
846
|
+
write_lease_id: task.lease.lease_id,
|
|
847
|
+
controller_registration_digest: request.registration_digest,
|
|
848
|
+
}))
|
|
849
|
+
.sort((left, right) => left.todo_id.localeCompare(right.todo_id));
|
|
850
|
+
return sign({
|
|
851
|
+
schema: 'lattice.adapter_running_inventory_response.v1',
|
|
852
|
+
request_id: request.request_id,
|
|
853
|
+
running_bindings: runningBindings,
|
|
854
|
+
inventory_digest: digestArtifact(runningBindings),
|
|
855
|
+
response_digest: '',
|
|
856
|
+
}, 'response_digest');
|
|
857
|
+
}
|
|
858
|
+
if (operation === 'barrier') {
|
|
859
|
+
const acks = [];
|
|
860
|
+
for (const binding of request.running_bindings) {
|
|
861
|
+
const task = tasks.get(binding.executor_handle);
|
|
862
|
+
if (task === undefined || task.packet.todo_id !== binding.todo_id) {
|
|
863
|
+
fail('SCRIPTED_BARRIER_REJECTED', 'barrierが未知のrunning bindingを含む');
|
|
864
|
+
}
|
|
865
|
+
// **barrierは静止の宣言である。** worker processを実際に止め、止まった木を読む。
|
|
866
|
+
// 走行中の作業を残したままackを返すと、「止まった」と言いながらworktreeが動き続ける。
|
|
867
|
+
try {
|
|
868
|
+
process.kill(task.worker.pid, 'SIGSTOP');
|
|
869
|
+
} catch (error) {
|
|
870
|
+
fail('SCRIPTED_BARRIER_REJECTED', 'worker processを止められない', {
|
|
871
|
+
pid: task.worker.pid, reason: String(error?.code ?? error?.message ?? error),
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
task.state = 'held';
|
|
875
|
+
// supervisorも同じ観測を独立に行い、3つのdigestの一致を要求する。**こちらが別の形で
|
|
876
|
+
// 作ると、両者が別のものを見ていても気づけない。** 形はdirect OS observerの契約に
|
|
877
|
+
// 揃え、入力(自分のprocess・自分の木)だけを独立に観測する。
|
|
878
|
+
const checkpoint = await captureWorktreeDiff({
|
|
879
|
+
worktreePath: task.worktreePath, baseSha: task.packet.base_sha,
|
|
880
|
+
});
|
|
881
|
+
const processObservationDigest = digestArtifact({
|
|
882
|
+
schema: 'lattice.direct_process_observation.v2',
|
|
883
|
+
root: {
|
|
884
|
+
pid: task.worker.pid,
|
|
885
|
+
parent_pid: process.pid,
|
|
886
|
+
process_start_identity_digest: task.worker.process_start_identity.identity_digest,
|
|
887
|
+
process_group_id: task.worker.process_group_id,
|
|
888
|
+
state: 'stopped',
|
|
889
|
+
},
|
|
890
|
+
children: [],
|
|
891
|
+
process_group_id: task.worker.process_group_id,
|
|
892
|
+
quiesced: true,
|
|
893
|
+
});
|
|
894
|
+
const worktreeFingerprintDigest = digestArtifact({
|
|
895
|
+
schema: 'lattice.direct_worktree_fingerprint.v1',
|
|
896
|
+
worktree_id: task.worktreeId,
|
|
897
|
+
worktree_realpath: task.worktreePath,
|
|
898
|
+
checkpoint_digest: checkpoint.checkpoint_digest,
|
|
899
|
+
});
|
|
900
|
+
acks.push(sign({
|
|
901
|
+
schema: 'lattice.executor_quiescence_ack.v1',
|
|
902
|
+
ack_id: `barrier-${binding.packet_digest.slice(0, 24)}`,
|
|
903
|
+
run_id: bootstrap.run_id,
|
|
904
|
+
todo_id: binding.todo_id,
|
|
905
|
+
executor_handle: binding.executor_handle,
|
|
906
|
+
worktree_id: binding.worktree_id,
|
|
907
|
+
plan_epoch: binding.plan_epoch,
|
|
908
|
+
packet_digest: binding.packet_digest,
|
|
909
|
+
write_lease_id: binding.write_lease_id,
|
|
910
|
+
barrier_control_digest: request.barrier_control_digest,
|
|
911
|
+
final_checkpoint_digest: checkpoint.checkpoint_digest,
|
|
912
|
+
process_observation_digest: processObservationDigest,
|
|
913
|
+
worktree_fingerprint_digest: worktreeFingerprintDigest,
|
|
914
|
+
supervisor_session_nonce_digest: sessionNonceDigest,
|
|
915
|
+
ack_digest: '',
|
|
916
|
+
}, 'ack_digest'));
|
|
917
|
+
}
|
|
918
|
+
return sign({
|
|
919
|
+
schema: 'lattice.adapter_barrier_response.v1',
|
|
920
|
+
request_id: request.request_id,
|
|
921
|
+
barrier_id: request.barrier_id,
|
|
922
|
+
quiescence_acks: acks,
|
|
923
|
+
response_digest: '',
|
|
924
|
+
}, 'response_digest');
|
|
925
|
+
}
|
|
926
|
+
if (operation === 'rebind') {
|
|
927
|
+
const rebind = request.rebind_packet;
|
|
928
|
+
const task = tasks.get(rebind.executor_handle);
|
|
929
|
+
if (task === undefined || task.worktreeId !== rebind.worktree_id
|
|
930
|
+
|| task.packet.todo_id !== rebind.todo_id) {
|
|
931
|
+
fail('SCRIPTED_REBIND_REJECTED', 'rebind packetが既存taskへ帰属しない');
|
|
932
|
+
}
|
|
933
|
+
const staged = request.staged_lease;
|
|
934
|
+
if (staged.todo_id !== rebind.todo_id || staged.plan_epoch !== rebind.new_plan_epoch
|
|
935
|
+
|| staged.packet_digest !== rebind.packet_digest) {
|
|
936
|
+
fail('SCRIPTED_REBIND_REJECTED', 'rebind packetとstaged leaseが一致しない');
|
|
937
|
+
}
|
|
938
|
+
stagedLeases.set(staged.lease_digest, structuredClone(staged));
|
|
939
|
+
task.packet.plan_epoch = rebind.new_plan_epoch;
|
|
940
|
+
currentEpoch = rebind.new_plan_epoch;
|
|
941
|
+
const ack = sign({
|
|
942
|
+
schema: 'lattice.executor_epoch_rebind_ack.v1',
|
|
943
|
+
ack_id: `rebind-${rebind.packet_digest.slice(0, 24)}`,
|
|
944
|
+
run_id: bootstrap.run_id,
|
|
945
|
+
todo_id: rebind.todo_id,
|
|
946
|
+
executor_handle: rebind.executor_handle,
|
|
947
|
+
worktree_id: rebind.worktree_id,
|
|
948
|
+
predecessor_epoch: rebind.new_plan_epoch - 1,
|
|
949
|
+
successor_epoch: rebind.new_plan_epoch,
|
|
950
|
+
// **holdされたworkerにreceiptは無い。** 作業を終えていないから止められているので、
|
|
951
|
+
// 完了の記録を前提にできない。前任packetのdigestはdispatch時のpacketが持っている。
|
|
952
|
+
predecessor_packet_digest: task.packet.packet_digest,
|
|
953
|
+
rebind_packet_digest: rebind.packet_digest,
|
|
954
|
+
new_write_lease_id: staged.lease_id,
|
|
955
|
+
supervisor_session_nonce_digest: sessionNonceDigest,
|
|
956
|
+
ack_digest: '',
|
|
957
|
+
}, 'ack_digest');
|
|
958
|
+
return sign({
|
|
959
|
+
schema: 'lattice.adapter_rebind_response.v1',
|
|
960
|
+
request_id: request.request_id,
|
|
961
|
+
rebind_ack: ack,
|
|
962
|
+
staged_lease_digest: staged.lease_digest,
|
|
963
|
+
response_digest: '',
|
|
964
|
+
}, 'response_digest');
|
|
965
|
+
}
|
|
966
|
+
if (operation === 'revoke') {
|
|
967
|
+
const available = new Set([...stagedLeases.keys(), ...armedLeases.keys()]);
|
|
968
|
+
if (request.lease_digests.some((digest) => !available.has(digest))) {
|
|
969
|
+
fail('SCRIPTED_REVOKE_REJECTED', 'revokeが未知のleaseを含む');
|
|
970
|
+
}
|
|
971
|
+
for (const leaseDigest of request.lease_digests) {
|
|
972
|
+
stagedLeases.delete(leaseDigest);
|
|
973
|
+
armedLeases.delete(leaseDigest);
|
|
974
|
+
}
|
|
975
|
+
for (const task of tasks.values()) {
|
|
976
|
+
if (request.lease_digests.includes(task.lease.lease_digest)) task.state = 'held';
|
|
977
|
+
}
|
|
978
|
+
return sign({
|
|
979
|
+
schema: 'lattice.adapter_revoke_response.v1',
|
|
980
|
+
request_id: request.request_id,
|
|
981
|
+
revoked_lease_digests: [...request.lease_digests],
|
|
982
|
+
residual_processes: [],
|
|
983
|
+
response_digest: '',
|
|
984
|
+
}, 'response_digest');
|
|
985
|
+
}
|
|
986
|
+
fail('SCRIPTED_UNKNOWN_OPERATION', `未知のcontroller operation: ${operation}`);
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
return Object.freeze({
|
|
990
|
+
bootstrap: structuredClone(bootstrap),
|
|
991
|
+
controllerId,
|
|
992
|
+
controllerSessionNonce,
|
|
993
|
+
descriptor: structuredClone(descriptor),
|
|
994
|
+
heartbeat,
|
|
995
|
+
reapWorkers,
|
|
996
|
+
leaseSetDigest() {
|
|
997
|
+
return digestArtifact([...stagedLeases.keys(), ...armedLeases.keys()].sort());
|
|
998
|
+
},
|
|
999
|
+
handshake(request) {
|
|
1000
|
+
if (!validateHandshakeRequest(request, bootstrap)) {
|
|
1001
|
+
fail('SCRIPTED_HANDSHAKE_INVALID', 'handshake requestがbootstrapへbindしない');
|
|
1002
|
+
}
|
|
1003
|
+
return sign({
|
|
1004
|
+
schema: 'lattice.adapter_controller_handshake_response.v1',
|
|
1005
|
+
request_id: request.request_id,
|
|
1006
|
+
run_id: bootstrap.run_id,
|
|
1007
|
+
challenge_digest: digestArtifact(request.challenge),
|
|
1008
|
+
controller_session_nonce: controllerSessionNonce,
|
|
1009
|
+
descriptor,
|
|
1010
|
+
response_digest: '',
|
|
1011
|
+
}, 'response_digest');
|
|
1012
|
+
},
|
|
1013
|
+
async request(operation, request) {
|
|
1014
|
+
if (!CONTROLLER_OPERATIONS.includes(operation)) {
|
|
1015
|
+
fail('SCRIPTED_UNKNOWN_OPERATION', `未知のcontroller operation: ${operation}`);
|
|
1016
|
+
}
|
|
1017
|
+
return responseFor(operation, request);
|
|
1018
|
+
},
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function readBootstrapFd(fd = 3) {
|
|
1023
|
+
const bytes = readFileSync(fd);
|
|
1024
|
+
if (bytes.length === 0 || bytes.length > MAX_DOCUMENT_BYTES) {
|
|
1025
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'bootstrap sizeが不正');
|
|
1026
|
+
}
|
|
1027
|
+
const text = bytes.toString('utf8');
|
|
1028
|
+
if (!text.endsWith('\n') || text.indexOf('\n') !== text.length - 1) {
|
|
1029
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'bootstrapはJSON 1 documentでなければならない');
|
|
1030
|
+
}
|
|
1031
|
+
let value;
|
|
1032
|
+
try {
|
|
1033
|
+
value = JSON.parse(text);
|
|
1034
|
+
} catch {
|
|
1035
|
+
fail('SCRIPTED_BOOTSTRAP_INVALID', 'bootstrap JSONが不正');
|
|
1036
|
+
}
|
|
1037
|
+
return value;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
export async function runScriptedAdapterController({
|
|
1041
|
+
runDir = process.cwd(),
|
|
1042
|
+
bootstrap = null,
|
|
1043
|
+
stderr = process.stderr,
|
|
1044
|
+
} = {}) {
|
|
1045
|
+
const resolvedBootstrap = bootstrap ?? await readBootstrapFd();
|
|
1046
|
+
const canonicalRunDir = await realpath(runDir);
|
|
1047
|
+
const repoRoot = await realpath(path.resolve(canonicalRunDir, '..', '..', '..'));
|
|
1048
|
+
const controller = await createScriptedAdapterController({
|
|
1049
|
+
bootstrap: resolvedBootstrap,
|
|
1050
|
+
runDir: canonicalRunDir,
|
|
1051
|
+
repoRoot,
|
|
1052
|
+
});
|
|
1053
|
+
const socketPath = path.join(canonicalRunDir, resolvedBootstrap.controller_socket_ref);
|
|
1054
|
+
const controllerDirectory = path.dirname(socketPath);
|
|
1055
|
+
await mkdir(controllerDirectory, { recursive: true, mode: 0o700 });
|
|
1056
|
+
await chmod(controllerDirectory, 0o700);
|
|
1057
|
+
let heartbeatTimer = null;
|
|
1058
|
+
let persistentSocket = null;
|
|
1059
|
+
let heartbeatSequence = 0;
|
|
1060
|
+
let registrationDigest = null;
|
|
1061
|
+
const send = (socket, document) => {
|
|
1062
|
+
socket.write(`${canonicalizeArtifact(document)}\n`);
|
|
1063
|
+
};
|
|
1064
|
+
const startHeartbeat = (socket, digest) => {
|
|
1065
|
+
persistentSocket = socket;
|
|
1066
|
+
registrationDigest = digest;
|
|
1067
|
+
if (heartbeatTimer !== null) return;
|
|
1068
|
+
heartbeatTimer = setInterval(() => {
|
|
1069
|
+
if (persistentSocket === null || persistentSocket.destroyed) return;
|
|
1070
|
+
heartbeatSequence += 1;
|
|
1071
|
+
send(persistentSocket, sign({
|
|
1072
|
+
schema: 'lattice.adapter_controller_heartbeat.v1',
|
|
1073
|
+
controller_id: controller.controllerId,
|
|
1074
|
+
registration_digest: registrationDigest,
|
|
1075
|
+
supervisor_session_nonce: resolvedBootstrap.supervisor_session_nonce,
|
|
1076
|
+
sequence: heartbeatSequence,
|
|
1077
|
+
lease_set_digest: controller.leaseSetDigest(),
|
|
1078
|
+
heartbeat_digest: '',
|
|
1079
|
+
}, 'heartbeat_digest'));
|
|
1080
|
+
}, controller.heartbeat.interval_ms);
|
|
1081
|
+
heartbeatTimer.unref();
|
|
1082
|
+
};
|
|
1083
|
+
const server = net.createServer((socket) => {
|
|
1084
|
+
socket.setEncoding('utf8');
|
|
1085
|
+
let buffer = '';
|
|
1086
|
+
let processing = Promise.resolve();
|
|
1087
|
+
socket.on('data', (chunk) => {
|
|
1088
|
+
buffer += chunk;
|
|
1089
|
+
if (Buffer.byteLength(buffer, 'utf8') > MAX_DOCUMENT_BYTES) {
|
|
1090
|
+
send(socket, controllerErrorArtifact(
|
|
1091
|
+
new ScriptedAdapterControllerError(
|
|
1092
|
+
'SCRIPTED_DOCUMENT_TOO_LARGE',
|
|
1093
|
+
'controller document上限を超えた',
|
|
1094
|
+
),
|
|
1095
|
+
));
|
|
1096
|
+
socket.destroy();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
while (buffer.includes('\n')) {
|
|
1100
|
+
const newline = buffer.indexOf('\n');
|
|
1101
|
+
const line = buffer.slice(0, newline);
|
|
1102
|
+
buffer = buffer.slice(newline + 1);
|
|
1103
|
+
processing = processing.then(async () => {
|
|
1104
|
+
let request;
|
|
1105
|
+
try {
|
|
1106
|
+
request = JSON.parse(line);
|
|
1107
|
+
if (request.schema === 'lattice.adapter_controller_handshake_request.v1') {
|
|
1108
|
+
send(socket, controller.handshake(request));
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const operation = REQUEST_SCHEMA_TO_OPERATION[request.schema];
|
|
1112
|
+
if (operation === undefined) {
|
|
1113
|
+
fail('SCRIPTED_UNKNOWN_OPERATION', `未知のrequest schema: ${String(request.schema)}`);
|
|
1114
|
+
}
|
|
1115
|
+
const response = await controller.request(operation, request);
|
|
1116
|
+
startHeartbeat(socket, request.registration_digest);
|
|
1117
|
+
send(socket, response);
|
|
1118
|
+
} catch (error) {
|
|
1119
|
+
send(socket, controllerErrorArtifact(error, request?.request_id));
|
|
1120
|
+
stderr.write(`${canonicalizeArtifact(controllerErrorArtifact(error, request?.request_id))}\n`);
|
|
1121
|
+
socket.destroy();
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
});
|
|
1126
|
+
socket.on('close', () => {
|
|
1127
|
+
if (socket === persistentSocket) {
|
|
1128
|
+
clearInterval(heartbeatTimer);
|
|
1129
|
+
heartbeatTimer = null;
|
|
1130
|
+
persistentSocket = null;
|
|
1131
|
+
}
|
|
1132
|
+
});
|
|
1133
|
+
});
|
|
1134
|
+
server.on('error', (error) => {
|
|
1135
|
+
stderr.write(`${canonicalizeArtifact(controllerErrorArtifact(error))}\n`);
|
|
1136
|
+
});
|
|
1137
|
+
await new Promise((resolve, reject) => {
|
|
1138
|
+
server.once('error', reject);
|
|
1139
|
+
// AF_UNIXのpath長上限へ一時repoのabsolute prefixを混入させない。supervisorも
|
|
1140
|
+
// run storeをcwdにして同じ固定relative refへ接続する。
|
|
1141
|
+
server.listen(resolvedBootstrap.controller_socket_ref, () => {
|
|
1142
|
+
server.off('error', reject);
|
|
1143
|
+
resolve();
|
|
1144
|
+
});
|
|
1145
|
+
});
|
|
1146
|
+
await chmod(socketPath, 0o600);
|
|
1147
|
+
let closed = false;
|
|
1148
|
+
const close = async () => {
|
|
1149
|
+
if (closed) return;
|
|
1150
|
+
closed = true;
|
|
1151
|
+
// 起こしたworkerを道連れにする。停止中のworkerは自分では終われないので、
|
|
1152
|
+
// ここで畳まないとrunもrepoも消えた後まで残り続ける。
|
|
1153
|
+
controller.reapWorkers();
|
|
1154
|
+
clearInterval(heartbeatTimer);
|
|
1155
|
+
persistentSocket?.destroy();
|
|
1156
|
+
await new Promise((resolve) => server.close(resolve));
|
|
1157
|
+
await rm(socketPath, { force: true });
|
|
1158
|
+
};
|
|
1159
|
+
return Object.freeze({ controller, socketPath, close });
|
|
1160
|
+
}
|