openplanr 1.13.2 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -4
- package/dist/cli/commands/artifact.d.ts.map +1 -1
- package/dist/cli/commands/artifact.js +17 -6
- package/dist/cli/commands/artifact.js.map +1 -1
- package/dist/cli/commands/operate.d.ts +3 -0
- package/dist/cli/commands/operate.d.ts.map +1 -0
- package/dist/cli/commands/operate.js +568 -0
- package/dist/cli/commands/operate.js.map +1 -0
- package/dist/cli/index.js +42 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/services/ai-service.d.ts +9 -2
- package/dist/services/ai-service.d.ts.map +1 -1
- package/dist/services/ai-service.js +14 -6
- package/dist/services/ai-service.js.map +1 -1
- package/dist/services/artifact-pipeline-service.d.ts +1 -0
- package/dist/services/artifact-pipeline-service.d.ts.map +1 -1
- package/dist/services/artifact-pipeline-service.js.map +1 -1
- package/dist/services/delta-apply-service.js +1 -1
- package/dist/services/delta-apply-service.js.map +1 -1
- package/dist/services/operate/advisors.d.ts +219 -0
- package/dist/services/operate/advisors.d.ts.map +1 -0
- package/dist/services/operate/advisors.js +745 -0
- package/dist/services/operate/advisors.js.map +1 -0
- package/dist/services/operate/artifact-route-generation.d.ts +111 -0
- package/dist/services/operate/artifact-route-generation.d.ts.map +1 -0
- package/dist/services/operate/artifact-route-generation.js +459 -0
- package/dist/services/operate/artifact-route-generation.js.map +1 -0
- package/dist/services/operate/artifacts.d.ts +26 -0
- package/dist/services/operate/artifacts.d.ts.map +1 -0
- package/dist/services/operate/artifacts.js +92 -0
- package/dist/services/operate/artifacts.js.map +1 -0
- package/dist/services/operate/canonical.d.ts +18 -0
- package/dist/services/operate/canonical.d.ts.map +1 -0
- package/dist/services/operate/canonical.js +157 -0
- package/dist/services/operate/canonical.js.map +1 -0
- package/dist/services/operate/config.d.ts +56 -0
- package/dist/services/operate/config.d.ts.map +1 -0
- package/dist/services/operate/config.js +644 -0
- package/dist/services/operate/config.js.map +1 -0
- package/dist/services/operate/consolidation.d.ts +18 -0
- package/dist/services/operate/consolidation.d.ts.map +1 -0
- package/dist/services/operate/consolidation.js +539 -0
- package/dist/services/operate/consolidation.js.map +1 -0
- package/dist/services/operate/doctor.d.ts +12 -0
- package/dist/services/operate/doctor.d.ts.map +1 -0
- package/dist/services/operate/doctor.js +307 -0
- package/dist/services/operate/doctor.js.map +1 -0
- package/dist/services/operate/engine.d.ts +53 -0
- package/dist/services/operate/engine.d.ts.map +1 -0
- package/dist/services/operate/engine.js +1097 -0
- package/dist/services/operate/engine.js.map +1 -0
- package/dist/services/operate/event-store.d.ts +57 -0
- package/dist/services/operate/event-store.d.ts.map +1 -0
- package/dist/services/operate/event-store.js +232 -0
- package/dist/services/operate/event-store.js.map +1 -0
- package/dist/services/operate/evidence-cache.d.ts +17 -0
- package/dist/services/operate/evidence-cache.d.ts.map +1 -0
- package/dist/services/operate/evidence-cache.js +96 -0
- package/dist/services/operate/evidence-cache.js.map +1 -0
- package/dist/services/operate/evidence-import.d.ts +40 -0
- package/dist/services/operate/evidence-import.d.ts.map +1 -0
- package/dist/services/operate/evidence-import.js +340 -0
- package/dist/services/operate/evidence-import.js.map +1 -0
- package/dist/services/operate/evidence-readiness.d.ts +8 -0
- package/dist/services/operate/evidence-readiness.d.ts.map +1 -0
- package/dist/services/operate/evidence-readiness.js +78 -0
- package/dist/services/operate/evidence-readiness.js.map +1 -0
- package/dist/services/operate/evidence.d.ts +71 -0
- package/dist/services/operate/evidence.d.ts.map +1 -0
- package/dist/services/operate/evidence.js +1022 -0
- package/dist/services/operate/evidence.js.map +1 -0
- package/dist/services/operate/ids.d.ts +7 -0
- package/dist/services/operate/ids.d.ts.map +1 -0
- package/dist/services/operate/ids.js +23 -0
- package/dist/services/operate/ids.js.map +1 -0
- package/dist/services/operate/index.d.ts +14 -0
- package/dist/services/operate/index.d.ts.map +1 -0
- package/dist/services/operate/index.js +881 -0
- package/dist/services/operate/index.js.map +1 -0
- package/dist/services/operate/journal.d.ts +34 -0
- package/dist/services/operate/journal.d.ts.map +1 -0
- package/dist/services/operate/journal.js +229 -0
- package/dist/services/operate/journal.js.map +1 -0
- package/dist/services/operate/legacy-import-service.d.ts +58 -0
- package/dist/services/operate/legacy-import-service.d.ts.map +1 -0
- package/dist/services/operate/legacy-import-service.js +922 -0
- package/dist/services/operate/legacy-import-service.js.map +1 -0
- package/dist/services/operate/lifecycle.d.ts +88 -0
- package/dist/services/operate/lifecycle.d.ts.map +1 -0
- package/dist/services/operate/lifecycle.js +556 -0
- package/dist/services/operate/lifecycle.js.map +1 -0
- package/dist/services/operate/lock-service.d.ts +37 -0
- package/dist/services/operate/lock-service.d.ts.map +1 -0
- package/dist/services/operate/lock-service.js +208 -0
- package/dist/services/operate/lock-service.js.map +1 -0
- package/dist/services/operate/maintenance.d.ts +38 -0
- package/dist/services/operate/maintenance.d.ts.map +1 -0
- package/dist/services/operate/maintenance.js +699 -0
- package/dist/services/operate/maintenance.js.map +1 -0
- package/dist/services/operate/outcomes.d.ts +48 -0
- package/dist/services/operate/outcomes.d.ts.map +1 -0
- package/dist/services/operate/outcomes.js +312 -0
- package/dist/services/operate/outcomes.js.map +1 -0
- package/dist/services/operate/pipeline-handoff.d.ts +138 -0
- package/dist/services/operate/pipeline-handoff.d.ts.map +1 -0
- package/dist/services/operate/pipeline-handoff.js +175 -0
- package/dist/services/operate/pipeline-handoff.js.map +1 -0
- package/dist/services/operate/projection-persistence.d.ts +55 -0
- package/dist/services/operate/projection-persistence.d.ts.map +1 -0
- package/dist/services/operate/projection-persistence.js +393 -0
- package/dist/services/operate/projection-persistence.js.map +1 -0
- package/dist/services/operate/projection.d.ts +4 -0
- package/dist/services/operate/projection.d.ts.map +1 -0
- package/dist/services/operate/projection.js +128 -0
- package/dist/services/operate/projection.js.map +1 -0
- package/dist/services/operate/protocol.d.ts +60 -0
- package/dist/services/operate/protocol.d.ts.map +1 -0
- package/dist/services/operate/protocol.js +62 -0
- package/dist/services/operate/protocol.js.map +1 -0
- package/dist/services/operate/read-only-providers.d.ts +38 -0
- package/dist/services/operate/read-only-providers.d.ts.map +1 -0
- package/dist/services/operate/read-only-providers.js +286 -0
- package/dist/services/operate/read-only-providers.js.map +1 -0
- package/dist/services/operate/redaction.d.ts +47 -0
- package/dist/services/operate/redaction.d.ts.map +1 -0
- package/dist/services/operate/redaction.js +279 -0
- package/dist/services/operate/redaction.js.map +1 -0
- package/dist/services/operate/reducer.d.ts +10 -0
- package/dist/services/operate/reducer.d.ts.map +1 -0
- package/dist/services/operate/reducer.js +11 -0
- package/dist/services/operate/reducer.js.map +1 -0
- package/dist/services/operate/routes.d.ts +49 -0
- package/dist/services/operate/routes.d.ts.map +1 -0
- package/dist/services/operate/routes.js +996 -0
- package/dist/services/operate/routes.js.map +1 -0
- package/dist/services/operate/shipment-observer.d.ts +14 -0
- package/dist/services/operate/shipment-observer.d.ts.map +1 -0
- package/dist/services/operate/shipment-observer.js +321 -0
- package/dist/services/operate/shipment-observer.js.map +1 -0
- package/dist/services/operate/stalled-item-service.d.ts +18 -0
- package/dist/services/operate/stalled-item-service.d.ts.map +1 -0
- package/dist/services/operate/stalled-item-service.js +121 -0
- package/dist/services/operate/stalled-item-service.js.map +1 -0
- package/dist/services/operate/subprocess-env.d.ts +4 -0
- package/dist/services/operate/subprocess-env.d.ts.map +1 -0
- package/dist/services/operate/subprocess-env.js +45 -0
- package/dist/services/operate/subprocess-env.js.map +1 -0
- package/dist/services/operate/types.d.ts +729 -0
- package/dist/services/operate/types.d.ts.map +1 -0
- package/dist/services/operate/types.js +13 -0
- package/dist/services/operate/types.js.map +1 -0
- package/dist/services/operate/workspace.d.ts +59 -0
- package/dist/services/operate/workspace.d.ts.map +1 -0
- package/dist/services/operate/workspace.js +316 -0
- package/dist/services/operate/workspace.js.map +1 -0
- package/dist/services/runtime-manager-service.d.ts.map +1 -1
- package/dist/services/runtime-manager-service.js +71 -2
- package/dist/services/runtime-manager-service.js.map +1 -1
- package/package.json +10 -4
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { mkdir, readdir, readFile, rename, rm, unlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { assertAdvisorOutputMatchesBrief, buildAdvisorOperatingContext, createOperatingAdvisorPack, } from './advisors.js';
|
|
5
|
+
import { canonicalDigest, canonicalize, sha256Digest } from './canonical.js';
|
|
6
|
+
import { operatingProjectKey } from './config.js';
|
|
7
|
+
import { buildChairEvidence } from './engine.js';
|
|
8
|
+
import { OperatingEventStore } from './event-store.js';
|
|
9
|
+
import { OperatingEvidenceCache } from './evidence-cache.js';
|
|
10
|
+
import { evaluateEvidenceReadiness } from './evidence-readiness.js';
|
|
11
|
+
import { assertCommittedOperatingView, recoverOperatingTransactions } from './journal.js';
|
|
12
|
+
import { withOperatingLock } from './lock-service.js';
|
|
13
|
+
import { assertOperatingArtifact, loadOperatingProtocol } from './protocol.js';
|
|
14
|
+
import { containsSecret, redactSensitiveText } from './redaction.js';
|
|
15
|
+
import { OPERATE_PROTOCOL_VERSION, OPERATE_SCHEMA_VERSION, OperateError, } from './types.js';
|
|
16
|
+
import { resolveContainedPath, resolveOperatingPaths } from './workspace.js';
|
|
17
|
+
async function atomicPrivateWrite(target, value) {
|
|
18
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
19
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
20
|
+
await writeFile(temporary, `${canonicalize(value)}\n`, { mode: 0o600 });
|
|
21
|
+
await rename(temporary, target);
|
|
22
|
+
}
|
|
23
|
+
async function atomicBytesWrite(target, value) {
|
|
24
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
25
|
+
const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
26
|
+
await writeFile(temporary, value, { mode: 0o600 });
|
|
27
|
+
await rename(temporary, target);
|
|
28
|
+
}
|
|
29
|
+
async function regularFiles(root) {
|
|
30
|
+
const files = [];
|
|
31
|
+
async function visit(directory) {
|
|
32
|
+
for (const entry of await readdir(directory, { withFileTypes: true }).catch(() => [])) {
|
|
33
|
+
const target = path.join(directory, entry.name);
|
|
34
|
+
if (entry.isDirectory())
|
|
35
|
+
await visit(target);
|
|
36
|
+
else if (entry.isFile())
|
|
37
|
+
files.push(target);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
await visit(root);
|
|
41
|
+
return files.sort();
|
|
42
|
+
}
|
|
43
|
+
function isInside(root, target) {
|
|
44
|
+
const relative = path.relative(path.resolve(root), path.resolve(target));
|
|
45
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
46
|
+
}
|
|
47
|
+
function metadataPath(relativePath) {
|
|
48
|
+
const normalized = relativePath.split(path.sep).join('/');
|
|
49
|
+
return redactSensitiveText(normalized).value;
|
|
50
|
+
}
|
|
51
|
+
async function fileMetadata(target, root, scope, affected) {
|
|
52
|
+
const bytes = await readFile(target);
|
|
53
|
+
return {
|
|
54
|
+
scope,
|
|
55
|
+
path: metadataPath(path.relative(root, target)),
|
|
56
|
+
digest: sha256Digest(bytes),
|
|
57
|
+
size: bytes.byteLength,
|
|
58
|
+
affected,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export async function operatingCacheAction(input) {
|
|
62
|
+
const paths = resolveOperatingPaths(input.projectRoot, {
|
|
63
|
+
localRoot: input.localRoot,
|
|
64
|
+
});
|
|
65
|
+
const preferences = await readFile(path.join(paths.localRoot, 'preferences.json'), 'utf8')
|
|
66
|
+
.then((raw) => JSON.parse(raw))
|
|
67
|
+
.catch(() => ({ sensitivityCeiling: 'internal' }));
|
|
68
|
+
const cache = new OperatingEvidenceCache(paths.evidence, preferences.sensitivityCeiling ?? 'internal');
|
|
69
|
+
if (input.action === 'status')
|
|
70
|
+
return cache.status();
|
|
71
|
+
if (!input.confirmed) {
|
|
72
|
+
throw new OperateError('E_OPERATE_AUTHORITY_REQUIRED', 'Purging machine-local evidence requires explicit confirmation.');
|
|
73
|
+
}
|
|
74
|
+
const removed = await cache.purgeExpired(new Date(), { all: true });
|
|
75
|
+
return { removed: removed.length, entries: removed };
|
|
76
|
+
}
|
|
77
|
+
function integrityKeyPath(projectRoot, localRoot) {
|
|
78
|
+
return path.join(resolveOperatingPaths(projectRoot, { localRoot }).localRoot, 'integrity.key');
|
|
79
|
+
}
|
|
80
|
+
async function loadIntegrityKey(projectRoot, localRoot) {
|
|
81
|
+
return readFile(integrityKeyPath(projectRoot, localRoot)).catch(() => null);
|
|
82
|
+
}
|
|
83
|
+
function checkpointSigner(key) {
|
|
84
|
+
const keyId = `local:${sha256Digest(key).slice(7, 23)}`;
|
|
85
|
+
return (payload) => ({
|
|
86
|
+
algorithm: 'hmac-sha256',
|
|
87
|
+
keyId,
|
|
88
|
+
value: createHmac('sha256', key).update(payload).digest('base64url'),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function checkpointKeyFingerprint(keyId) {
|
|
92
|
+
return `digest:${sha256Digest(Buffer.from(keyId)).slice('sha256:'.length)}`;
|
|
93
|
+
}
|
|
94
|
+
export async function operatingIntegrityAction(input) {
|
|
95
|
+
const store = new OperatingEventStore(input.projectRoot, {
|
|
96
|
+
localRoot: input.localRoot,
|
|
97
|
+
});
|
|
98
|
+
const replay = await store.replay();
|
|
99
|
+
if (input.action === 'enable') {
|
|
100
|
+
if (!input.confirmed) {
|
|
101
|
+
throw new OperateError('E_OPERATE_AUTHORITY_REQUIRED', 'Enabling signed checkpoints requires explicit confirmation.');
|
|
102
|
+
}
|
|
103
|
+
const initial = replay.eventHead;
|
|
104
|
+
return withOperatingLock(input.projectRoot, {
|
|
105
|
+
projectKey: operatingProjectKey(input.projectRoot),
|
|
106
|
+
expectedEventHead: initial,
|
|
107
|
+
currentEventHead: initial,
|
|
108
|
+
localRoot: input.localRoot,
|
|
109
|
+
}, async () => {
|
|
110
|
+
const target = integrityKeyPath(input.projectRoot, input.localRoot);
|
|
111
|
+
let key = await loadIntegrityKey(input.projectRoot, input.localRoot);
|
|
112
|
+
if (!key) {
|
|
113
|
+
key = randomBytes(32);
|
|
114
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
115
|
+
await writeFile(target, key, { mode: 0o600, flag: 'wx' });
|
|
116
|
+
}
|
|
117
|
+
const checkpoint = await store.writeCheckpoint(await store.state(), {
|
|
118
|
+
signer: checkpointSigner(key),
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
status: checkpoint.integrity.status,
|
|
122
|
+
keyId: checkpoint.integrity.status === 'signed' ? checkpoint.integrity.signature.keyId : null,
|
|
123
|
+
checkpoint: '.planr/operate/checkpoints/current.json',
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const key = await loadIntegrityKey(input.projectRoot, input.localRoot);
|
|
128
|
+
const checkpoint = await store.readCheckpoint(key
|
|
129
|
+
? {
|
|
130
|
+
requireSignatureVerification: true,
|
|
131
|
+
verifySignature(payload, signature) {
|
|
132
|
+
if (signature.algorithm !== 'hmac-sha256')
|
|
133
|
+
return false;
|
|
134
|
+
const actual = checkpointSigner(key)(payload);
|
|
135
|
+
const left = Buffer.from(actual.value);
|
|
136
|
+
const right = Buffer.from(signature.value);
|
|
137
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
: {});
|
|
141
|
+
return {
|
|
142
|
+
eventHead: replay.eventHead,
|
|
143
|
+
checkpoint: checkpoint
|
|
144
|
+
? {
|
|
145
|
+
eventHead: checkpoint.eventHead,
|
|
146
|
+
stateHash: checkpoint.stateHash,
|
|
147
|
+
integrity: checkpoint.integrity,
|
|
148
|
+
verified: checkpoint.integrity.status === 'hash' ||
|
|
149
|
+
(checkpoint.integrity.status === 'signed' && Boolean(key)),
|
|
150
|
+
}
|
|
151
|
+
: null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export async function exportOperatingDiagnostics(input) {
|
|
155
|
+
await assertCommittedOperatingView(input.projectRoot, {
|
|
156
|
+
localRoot: input.localRoot,
|
|
157
|
+
});
|
|
158
|
+
const store = new OperatingEventStore(input.projectRoot, {
|
|
159
|
+
localRoot: input.localRoot,
|
|
160
|
+
});
|
|
161
|
+
const state = await store.state();
|
|
162
|
+
const replay = await store.replay();
|
|
163
|
+
const checkpoint = await store.readCheckpoint();
|
|
164
|
+
const output = input.output ?? '.planr/operate/diagnostics.json';
|
|
165
|
+
const target = await resolveContainedPath(input.projectRoot, output);
|
|
166
|
+
const diagnostic = {
|
|
167
|
+
schemaVersion: '1.0.0',
|
|
168
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
169
|
+
generatedAt: new Date().toISOString(),
|
|
170
|
+
eventHead: replay.eventHead,
|
|
171
|
+
checkpoint: checkpoint
|
|
172
|
+
? {
|
|
173
|
+
eventHead: checkpoint.eventHead,
|
|
174
|
+
stateHash: checkpoint.stateHash,
|
|
175
|
+
integrity: checkpoint.integrity,
|
|
176
|
+
}
|
|
177
|
+
: null,
|
|
178
|
+
summary: state.summary,
|
|
179
|
+
paths: {
|
|
180
|
+
commitSafe: '.planr/operate',
|
|
181
|
+
local: '~/.planr/operate/<project-hash>',
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
185
|
+
await writeFile(target, `${canonicalize(diagnostic)}\n`, { mode: 0o600 });
|
|
186
|
+
return { path: output, digest: canonicalDigest(diagnostic) };
|
|
187
|
+
}
|
|
188
|
+
export async function repairOperatingSecurity(input) {
|
|
189
|
+
const paths = resolveOperatingPaths(input.projectRoot, {
|
|
190
|
+
localRoot: input.localRoot,
|
|
191
|
+
});
|
|
192
|
+
const affected = [];
|
|
193
|
+
for (const target of await regularFiles(paths.root)) {
|
|
194
|
+
const relative = metadataPath(path.relative(input.projectRoot, target));
|
|
195
|
+
const raw = await readFile(target, 'utf8').catch(() => '');
|
|
196
|
+
if (containsSecret(raw))
|
|
197
|
+
affected.push(relative);
|
|
198
|
+
}
|
|
199
|
+
const pendingTransactions = await assertCommittedOperatingView(input.projectRoot, {
|
|
200
|
+
localRoot: input.localRoot,
|
|
201
|
+
}).then(() => false, () => true);
|
|
202
|
+
const preview = {
|
|
203
|
+
affected,
|
|
204
|
+
pendingTransactions,
|
|
205
|
+
actions: [
|
|
206
|
+
'recover incomplete local journals',
|
|
207
|
+
'hold the project writer lock for the complete repair',
|
|
208
|
+
'purge machine-local evidence, advisor, cache, journal, transaction, and prior quarantine data',
|
|
209
|
+
...(affected.length
|
|
210
|
+
? [
|
|
211
|
+
'record metadata-only quarantine evidence, purge affected records, and require explicit Git-history remediation',
|
|
212
|
+
]
|
|
213
|
+
: []),
|
|
214
|
+
],
|
|
215
|
+
};
|
|
216
|
+
if (!input.confirmed)
|
|
217
|
+
return preview;
|
|
218
|
+
const store = new OperatingEventStore(input.projectRoot, {
|
|
219
|
+
localRoot: input.localRoot,
|
|
220
|
+
});
|
|
221
|
+
const initial = await store.replay();
|
|
222
|
+
return withOperatingLock(input.projectRoot, {
|
|
223
|
+
projectKey: operatingProjectKey(input.projectRoot),
|
|
224
|
+
expectedEventHead: initial.eventHead,
|
|
225
|
+
currentEventHead: initial.eventHead,
|
|
226
|
+
localRoot: input.localRoot,
|
|
227
|
+
}, async (lock) => {
|
|
228
|
+
// The project writer lock is held before journal recovery or any purge.
|
|
229
|
+
// If another operating mutation starts while repair is active, it fails
|
|
230
|
+
// with E_OPERATE_CYCLE_ACTIVE instead of racing the discontinuity.
|
|
231
|
+
const lockedReplay = await store.replay();
|
|
232
|
+
lock.assertEventHead(lockedReplay.eventHead);
|
|
233
|
+
const recovered = await recoverOperatingTransactions(input.projectRoot, {
|
|
234
|
+
localRoot: input.localRoot,
|
|
235
|
+
});
|
|
236
|
+
const oldCheckpoint = await store.readCheckpoint().catch(() => null);
|
|
237
|
+
const operatingFiles = await regularFiles(paths.root);
|
|
238
|
+
const freshAffectedTargets = [];
|
|
239
|
+
for (const target of operatingFiles) {
|
|
240
|
+
const raw = await readFile(target, 'utf8').catch(() => '');
|
|
241
|
+
if (containsSecret(raw))
|
|
242
|
+
freshAffectedTargets.push(target);
|
|
243
|
+
}
|
|
244
|
+
const freshAffected = freshAffectedTargets
|
|
245
|
+
.map((target) => metadataPath(path.relative(input.projectRoot, target)))
|
|
246
|
+
.sort();
|
|
247
|
+
const purgeRoots = [
|
|
248
|
+
paths.cache,
|
|
249
|
+
paths.evidence,
|
|
250
|
+
paths.advisors,
|
|
251
|
+
paths.journals,
|
|
252
|
+
paths.transactions,
|
|
253
|
+
paths.quarantine,
|
|
254
|
+
];
|
|
255
|
+
const localFiles = await regularFiles(paths.localRoot);
|
|
256
|
+
const localPurgeTargets = new Set(localFiles.filter((target) => purgeRoots.some((root) => isInside(root, target))));
|
|
257
|
+
for (const target of localFiles) {
|
|
258
|
+
if (isInside(paths.locks, target) ||
|
|
259
|
+
target === integrityKeyPath(input.projectRoot, input.localRoot)) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const raw = await readFile(target, 'utf8').catch(() => '');
|
|
263
|
+
if (containsSecret(raw))
|
|
264
|
+
localPurgeTargets.add(target);
|
|
265
|
+
}
|
|
266
|
+
if (freshAffected.length > 0) {
|
|
267
|
+
const recoveryId = `RCV-${randomUUID()}`;
|
|
268
|
+
const confirmedAt = new Date().toISOString();
|
|
269
|
+
const quarantineRoot = path.join(paths.quarantine, recoveryId);
|
|
270
|
+
const commitSafeMetadata = await Promise.all(operatingFiles.map((target) => fileMetadata(target, input.projectRoot, 'commit-safe', freshAffectedTargets.includes(target))));
|
|
271
|
+
const localMetadata = await Promise.all([...localPurgeTargets]
|
|
272
|
+
.sort()
|
|
273
|
+
.map((target) => fileMetadata(target, paths.localRoot, 'machine-local', true)));
|
|
274
|
+
for (const root of purgeRoots) {
|
|
275
|
+
await rm(root, { recursive: true, force: true });
|
|
276
|
+
}
|
|
277
|
+
for (const target of localPurgeTargets) {
|
|
278
|
+
await unlink(target).catch(() => undefined);
|
|
279
|
+
}
|
|
280
|
+
const quarantineManifest = {
|
|
281
|
+
implementation: 'openplanr-operate-security-quarantine',
|
|
282
|
+
state: 'quarantined',
|
|
283
|
+
recoveryId,
|
|
284
|
+
createdAt: confirmedAt,
|
|
285
|
+
oldHead: initial.eventHead,
|
|
286
|
+
affectedPaths: freshAffected,
|
|
287
|
+
files: [...commitSafeMetadata, ...localMetadata].sort((left, right) => left.scope.localeCompare(right.scope) || left.path.localeCompare(right.path)),
|
|
288
|
+
};
|
|
289
|
+
const quarantineManifestDigest = canonicalDigest(quarantineManifest);
|
|
290
|
+
await atomicPrivateWrite(path.join(quarantineRoot, 'manifest.json'), quarantineManifest);
|
|
291
|
+
await input.faultInjector?.('project-quarantined');
|
|
292
|
+
const preserved = new Set([paths.config, paths.charter, paths.workspace]);
|
|
293
|
+
for (const target of operatingFiles) {
|
|
294
|
+
if (preserved.has(target)) {
|
|
295
|
+
const raw = await readFile(target, 'utf8');
|
|
296
|
+
if (containsSecret(raw)) {
|
|
297
|
+
await atomicBytesWrite(target, redactSensitiveText(raw).value);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
await unlink(target).catch(() => undefined);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
await mkdir(path.dirname(paths.events), {
|
|
305
|
+
recursive: true,
|
|
306
|
+
mode: 0o700,
|
|
307
|
+
});
|
|
308
|
+
await atomicBytesWrite(paths.events, '');
|
|
309
|
+
const reason = 'Explicit authority confirmed sensitive-state quarantine and event-chain re-genesis.';
|
|
310
|
+
const guidance = [
|
|
311
|
+
'Rotate every exposed credential immediately.',
|
|
312
|
+
'Remove affected bytes from Git history with an approved history-rewrite procedure.',
|
|
313
|
+
'Notify repository collaborators before force-updating protected branches.',
|
|
314
|
+
'Run `planr operate integrity status` and a fresh deep operating cycle.',
|
|
315
|
+
].join(' ');
|
|
316
|
+
const record = {
|
|
317
|
+
kind: 'operating-recovery-record',
|
|
318
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
319
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
320
|
+
id: recoveryId,
|
|
321
|
+
transactionId: null,
|
|
322
|
+
action: 'restore-backup',
|
|
323
|
+
reason,
|
|
324
|
+
previewDigest: canonicalDigest(preview),
|
|
325
|
+
fromHead: initial.eventHead,
|
|
326
|
+
toHead: { sequence: 1, hash: null },
|
|
327
|
+
outcome: 'recovered',
|
|
328
|
+
confirmedBy: 'operate-cli',
|
|
329
|
+
createdAt: confirmedAt,
|
|
330
|
+
};
|
|
331
|
+
await assertOperatingArtifact('operating-recovery-record', record);
|
|
332
|
+
const saved = await store.putRecord('recovery', record, { correlationId: recoveryId, createdAt: confirmedAt });
|
|
333
|
+
const oldCheckpointSummary = oldCheckpoint
|
|
334
|
+
? {
|
|
335
|
+
stateHash: oldCheckpoint.stateHash,
|
|
336
|
+
integrityStatus: oldCheckpoint.integrity.status,
|
|
337
|
+
keyId: oldCheckpoint.integrity.status === 'signed'
|
|
338
|
+
? checkpointKeyFingerprint(oldCheckpoint.integrity.signature.keyId)
|
|
339
|
+
: null,
|
|
340
|
+
}
|
|
341
|
+
: null;
|
|
342
|
+
const event = await store.append({
|
|
343
|
+
type: 'security.discontinuity',
|
|
344
|
+
cycleId: 'CYCLE-000',
|
|
345
|
+
entityId: recoveryId,
|
|
346
|
+
timestamp: confirmedAt,
|
|
347
|
+
actor: { kind: 'human', id: 'operate-cli' },
|
|
348
|
+
correlationId: recoveryId,
|
|
349
|
+
expectedHead: null,
|
|
350
|
+
payload: {
|
|
351
|
+
oldHead: initial.eventHead,
|
|
352
|
+
oldCheckpoint: oldCheckpointSummary,
|
|
353
|
+
authority: {
|
|
354
|
+
kind: 'human',
|
|
355
|
+
id: 'operate-cli',
|
|
356
|
+
confirmedAt,
|
|
357
|
+
},
|
|
358
|
+
remediation: {
|
|
359
|
+
reasonDigest: canonicalDigest(reason),
|
|
360
|
+
guidanceDigest: canonicalDigest(guidance),
|
|
361
|
+
affectedPathsDigest: canonicalDigest(freshAffected),
|
|
362
|
+
quarantineManifestDigest,
|
|
363
|
+
},
|
|
364
|
+
recoveryRecordDigest: saved.digest,
|
|
365
|
+
requiresSignedCheckpoint: true,
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
const next = { sequence: event.sequence, hash: event.eventHash };
|
|
369
|
+
await lock.advanceEventHead(initial.eventHead, next);
|
|
370
|
+
let key = await loadIntegrityKey(input.projectRoot, input.localRoot);
|
|
371
|
+
if (!key) {
|
|
372
|
+
key = randomBytes(32);
|
|
373
|
+
await atomicBytesWrite(integrityKeyPath(input.projectRoot, input.localRoot), key);
|
|
374
|
+
}
|
|
375
|
+
const checkpoint = await store.writeCheckpoint(await store.state(), {
|
|
376
|
+
signer: checkpointSigner(key),
|
|
377
|
+
});
|
|
378
|
+
return {
|
|
379
|
+
recoveredTransactions: recovered,
|
|
380
|
+
purgedEntries: localPurgeTargets.size,
|
|
381
|
+
record,
|
|
382
|
+
quarantine: {
|
|
383
|
+
root: quarantineRoot,
|
|
384
|
+
manifestDigest: quarantineManifestDigest,
|
|
385
|
+
fileCount: quarantineManifest.files.length,
|
|
386
|
+
},
|
|
387
|
+
checkpoint: {
|
|
388
|
+
integrity: checkpoint.integrity,
|
|
389
|
+
path: '.planr/operate/checkpoints/current.json',
|
|
390
|
+
},
|
|
391
|
+
guidance,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
for (const root of purgeRoots) {
|
|
395
|
+
await rm(root, { recursive: true, force: true });
|
|
396
|
+
}
|
|
397
|
+
for (const target of localPurgeTargets) {
|
|
398
|
+
await unlink(target).catch(() => undefined);
|
|
399
|
+
}
|
|
400
|
+
const record = {
|
|
401
|
+
kind: 'operating-recovery-record',
|
|
402
|
+
schemaVersion: OPERATE_SCHEMA_VERSION,
|
|
403
|
+
protocolVersion: OPERATE_PROTOCOL_VERSION,
|
|
404
|
+
id: `RCV-${randomUUID()}`,
|
|
405
|
+
transactionId: null,
|
|
406
|
+
action: 'recover-journal',
|
|
407
|
+
reason: 'Explicit security repair recovered local journals and purged local evidence.',
|
|
408
|
+
previewDigest: canonicalDigest(preview),
|
|
409
|
+
fromHead: initial.eventHead,
|
|
410
|
+
toHead: initial.eventHead,
|
|
411
|
+
outcome: 'recovered',
|
|
412
|
+
confirmedBy: 'operate-cli',
|
|
413
|
+
createdAt: new Date().toISOString(),
|
|
414
|
+
};
|
|
415
|
+
await assertOperatingArtifact('operating-recovery-record', record);
|
|
416
|
+
const saved = await store.putRecord('recovery', record, {
|
|
417
|
+
correlationId: record.id,
|
|
418
|
+
});
|
|
419
|
+
const event = await store.append({
|
|
420
|
+
type: 'recovery.performed',
|
|
421
|
+
cycleId: (await store.state()).summary.currentCycleId ?? 'CYCLE-000',
|
|
422
|
+
entityId: record.id,
|
|
423
|
+
payload: { recordDigest: saved.digest },
|
|
424
|
+
expectedHead: initial.eventHead.hash,
|
|
425
|
+
actor: { kind: 'human', id: 'operate-cli' },
|
|
426
|
+
});
|
|
427
|
+
const next = { sequence: event.sequence, hash: event.eventHash };
|
|
428
|
+
await lock.advanceEventHead(initial.eventHead, next);
|
|
429
|
+
await store.writeCheckpoint(await store.state());
|
|
430
|
+
return {
|
|
431
|
+
recoveredTransactions: recovered,
|
|
432
|
+
purgedEntries: localPurgeTargets.size,
|
|
433
|
+
record,
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function adapterSessionPath(projectRoot, cycleId, localRoot) {
|
|
438
|
+
return path.join(resolveOperatingPaths(projectRoot, { localRoot }).advisors, `${cycleId}.json`);
|
|
439
|
+
}
|
|
440
|
+
async function persistedAdapterEvidence(store, cycleId) {
|
|
441
|
+
const replay = await store.replay();
|
|
442
|
+
const event = [...replay.events]
|
|
443
|
+
.reverse()
|
|
444
|
+
.find((candidate) => candidate.cycleId === cycleId &&
|
|
445
|
+
candidate.type === 'evidence.collected' &&
|
|
446
|
+
typeof candidate.payload.recordDigest === 'string');
|
|
447
|
+
if (!event) {
|
|
448
|
+
throw new OperateError('E_OPERATE_EVIDENCE_NOT_READY', `Cycle ${cycleId} has no committed evidence snapshot for native dispatch.`);
|
|
449
|
+
}
|
|
450
|
+
const record = await store.readRecord(event.payload.recordDigest);
|
|
451
|
+
if (record.recordType !== 'evidence-metadata') {
|
|
452
|
+
throw new OperateError('E_OPERATE_STATE_INVALID', `Cycle ${cycleId} evidence references an unexpected record type.`);
|
|
453
|
+
}
|
|
454
|
+
return assertOperatingArtifact('operating-evidence', record.content);
|
|
455
|
+
}
|
|
456
|
+
async function persistedAdapterResults(store, cycleId) {
|
|
457
|
+
const replay = await store.replay();
|
|
458
|
+
const byRole = new Map();
|
|
459
|
+
for (const event of replay.events) {
|
|
460
|
+
if (event.cycleId !== cycleId ||
|
|
461
|
+
event.type !== 'advisory.recorded' ||
|
|
462
|
+
typeof event.payload.recordDigest !== 'string') {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const record = await store.readRecord(event.payload.recordDigest);
|
|
466
|
+
if (record.recordType !== 'advisor-result') {
|
|
467
|
+
throw new OperateError('E_OPERATE_STATE_INVALID', `Cycle ${cycleId} advisor event references an unexpected record type.`);
|
|
468
|
+
}
|
|
469
|
+
const result = await assertOperatingArtifact('operating-role-result', record.content);
|
|
470
|
+
byRole.set(result.roleId, result);
|
|
471
|
+
}
|
|
472
|
+
return [...byRole.values()].sort((left, right) => left.roleId.localeCompare(right.roleId));
|
|
473
|
+
}
|
|
474
|
+
async function readAdapterSession(projectRoot, cycleId, localRoot) {
|
|
475
|
+
const session = JSON.parse(await readFile(adapterSessionPath(projectRoot, cycleId, localRoot), 'utf8'));
|
|
476
|
+
if (session.implementation !== 'openplanr-operate-adapter' ||
|
|
477
|
+
Date.parse(session.expiresAt) <= Date.now()) {
|
|
478
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', 'Adapter session is invalid or expired.');
|
|
479
|
+
}
|
|
480
|
+
return session;
|
|
481
|
+
}
|
|
482
|
+
function assertAdapterBinding(session, lease, idempotencyKey) {
|
|
483
|
+
if (session.lease !== lease || session.idempotencyKey !== idempotencyKey) {
|
|
484
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Adapter lease or idempotency binding does not match the prepared session.');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
export async function operateAdapterLifecycle(input) {
|
|
488
|
+
if (!input.cycleId || !input.idempotencyKey) {
|
|
489
|
+
throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Adapter calls require --cycle-id and --idempotency-key.');
|
|
490
|
+
}
|
|
491
|
+
const target = adapterSessionPath(input.projectRoot, input.cycleId, input.localRoot);
|
|
492
|
+
if (input.action === 'prepare') {
|
|
493
|
+
if (!input.evidenceDigest?.startsWith('sha256:')) {
|
|
494
|
+
throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Adapter prepare requires --evidence-digest.');
|
|
495
|
+
}
|
|
496
|
+
const store = new OperatingEventStore(input.projectRoot, {
|
|
497
|
+
localRoot: input.localRoot,
|
|
498
|
+
});
|
|
499
|
+
const state = await store.state();
|
|
500
|
+
const cycle = state.cycles.find((record) => record.id === input.cycleId);
|
|
501
|
+
if (!cycle || !['advising', 'blocked'].includes(cycle.state)) {
|
|
502
|
+
throw new OperateError('E_OPERATE_STATE_INVALID', 'Adapter prepare requires an advising or blocked cycle.');
|
|
503
|
+
}
|
|
504
|
+
const existing = await readFile(target, 'utf8')
|
|
505
|
+
.then((raw) => JSON.parse(raw))
|
|
506
|
+
.catch(() => null);
|
|
507
|
+
if (existing?.idempotencyKey === input.idempotencyKey)
|
|
508
|
+
return existing;
|
|
509
|
+
const protocol = await loadOperatingProtocol();
|
|
510
|
+
const knownRoles = new Set(protocol.listOperatingRoles().map((role) => role.id));
|
|
511
|
+
const enabledRoles = (Array.isArray(cycle.enabledRoles) ? cycle.enabledRoles.map(String) : []).filter((role) => knownRoles.has(role));
|
|
512
|
+
const requestedRoles = input.role
|
|
513
|
+
? input.role
|
|
514
|
+
.split(',')
|
|
515
|
+
.map((role) => role.trim())
|
|
516
|
+
.filter(Boolean)
|
|
517
|
+
: enabledRoles.filter((role) => role !== 'chair');
|
|
518
|
+
const unknownRoles = requestedRoles.filter((role) => !knownRoles.has(role));
|
|
519
|
+
if (unknownRoles.length > 0) {
|
|
520
|
+
throw new OperateError('E_OPERATE_CONFIG_INVALID', `Adapter prepare contains unknown roles: ${unknownRoles.sort().join(', ')}.`);
|
|
521
|
+
}
|
|
522
|
+
const roles = [...new Set(requestedRoles)]
|
|
523
|
+
.filter((role) => knownRoles.has(role))
|
|
524
|
+
.sort();
|
|
525
|
+
if (roles.length === 0) {
|
|
526
|
+
throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Adapter prepare requires at least one bound advisor role.');
|
|
527
|
+
}
|
|
528
|
+
if (roles.some((role) => !enabledRoles.includes(role))) {
|
|
529
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Adapter prepare cannot widen the cycle enabled-role set.');
|
|
530
|
+
}
|
|
531
|
+
if (roles.includes('chair') && roles.length !== 1) {
|
|
532
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Chair dispatch must be prepared separately after independent advisor results.');
|
|
533
|
+
}
|
|
534
|
+
const baseEvidence = await persistedAdapterEvidence(store, input.cycleId);
|
|
535
|
+
if (baseEvidence.fingerprint !== input.evidenceDigest) {
|
|
536
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Adapter evidence digest does not match the committed cycle snapshot.');
|
|
537
|
+
}
|
|
538
|
+
const roleEvidence = roles[0] === 'chair'
|
|
539
|
+
? buildChairEvidence(baseEvidence, await persistedAdapterResults(store, input.cycleId), new Date().toISOString())
|
|
540
|
+
: baseEvidence;
|
|
541
|
+
const readiness = await evaluateEvidenceReadiness({
|
|
542
|
+
cycleId: input.cycleId,
|
|
543
|
+
evidence: roleEvidence,
|
|
544
|
+
enabledRoles: roles,
|
|
545
|
+
now: new Date(),
|
|
546
|
+
});
|
|
547
|
+
const unready = readiness.roles.filter((role) => !role.modelCallAllowed);
|
|
548
|
+
if (unready.length > 0) {
|
|
549
|
+
throw new OperateError('E_OPERATE_EVIDENCE_NOT_READY', `Native advisor preparation is blocked for: ${unready
|
|
550
|
+
.map((role) => role.roleId)
|
|
551
|
+
.sort()
|
|
552
|
+
.join(', ')}.`);
|
|
553
|
+
}
|
|
554
|
+
const context = await buildAdvisorOperatingContext({
|
|
555
|
+
charterPath: resolveOperatingPaths(input.projectRoot, {
|
|
556
|
+
localRoot: input.localRoot,
|
|
557
|
+
}).charter,
|
|
558
|
+
state,
|
|
559
|
+
cycleId: input.cycleId,
|
|
560
|
+
});
|
|
561
|
+
const rolePacks = Object.fromEntries(await Promise.all(readiness.roles.map(async (role) => [
|
|
562
|
+
role.roleId,
|
|
563
|
+
await createOperatingAdvisorPack({
|
|
564
|
+
cycleId: input.cycleId,
|
|
565
|
+
role,
|
|
566
|
+
evidence: roleEvidence,
|
|
567
|
+
context,
|
|
568
|
+
}),
|
|
569
|
+
])));
|
|
570
|
+
const roleBriefs = Object.fromEntries(Object.entries(rolePacks).map(([role, pack]) => [role, pack.roleBrief]));
|
|
571
|
+
const session = {
|
|
572
|
+
implementation: 'openplanr-operate-adapter',
|
|
573
|
+
cycleId: input.cycleId,
|
|
574
|
+
evidenceDigest: input.evidenceDigest,
|
|
575
|
+
lease: randomBytes(32).toString('base64url'),
|
|
576
|
+
idempotencyKey: input.idempotencyKey,
|
|
577
|
+
state: 'prepared',
|
|
578
|
+
expiresAt: new Date(Date.now() + 15 * 60 * 1_000).toISOString(),
|
|
579
|
+
roles,
|
|
580
|
+
recordedRoles: [],
|
|
581
|
+
roleBriefs,
|
|
582
|
+
rolePacks,
|
|
583
|
+
roleInputDigests: Object.fromEntries(roles.map((role) => [role, rolePacks[role].inputDigest])),
|
|
584
|
+
};
|
|
585
|
+
await atomicPrivateWrite(target, session);
|
|
586
|
+
return session;
|
|
587
|
+
}
|
|
588
|
+
if (!input.lease) {
|
|
589
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Adapter lease is required.');
|
|
590
|
+
}
|
|
591
|
+
const session = await readAdapterSession(input.projectRoot, input.cycleId, input.localRoot);
|
|
592
|
+
assertAdapterBinding(session, input.lease, input.idempotencyKey);
|
|
593
|
+
if (input.action === 'resume')
|
|
594
|
+
return session;
|
|
595
|
+
if (input.action === 'cancel') {
|
|
596
|
+
const cancelled = { ...session, state: 'cancelled' };
|
|
597
|
+
await atomicPrivateWrite(target, cancelled);
|
|
598
|
+
return cancelled;
|
|
599
|
+
}
|
|
600
|
+
if (input.action === 'record') {
|
|
601
|
+
if (!input.role || !input.stdin) {
|
|
602
|
+
throw new OperateError('E_OPERATE_CONFIG_INVALID', 'Adapter record requires --role and JSON on stdin.');
|
|
603
|
+
}
|
|
604
|
+
if (!session.roles.includes(input.role)) {
|
|
605
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Role ${input.role} was not bound by adapter prepare.`);
|
|
606
|
+
}
|
|
607
|
+
const result = JSON.parse(input.stdin);
|
|
608
|
+
await assertOperatingArtifact('operating-role-result', result);
|
|
609
|
+
if (result.cycleId !== input.cycleId || result.roleId !== input.role) {
|
|
610
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Recorded advisor output does not match its cycle and role binding.');
|
|
611
|
+
}
|
|
612
|
+
assertAdvisorOutputMatchesBrief(session.roleBriefs[input.role], result);
|
|
613
|
+
if (result.inputDigest !== session.roleInputDigests[input.role]) {
|
|
614
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Recorded advisor output does not match its role/evidence input digest.');
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
(await loadOperatingProtocol()).validateOperatingRoleResultDigest(result);
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', 'Recorded advisor output resultDigest does not match its canonical content.');
|
|
621
|
+
}
|
|
622
|
+
const existingResult = await readFile(path.join(path.dirname(target), `${input.cycleId}.${input.role}.json`), 'utf8')
|
|
623
|
+
.then((raw) => JSON.parse(raw))
|
|
624
|
+
.catch(() => null);
|
|
625
|
+
if (existingResult && existingResult.resultDigest !== result.resultDigest) {
|
|
626
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Role ${input.role} already recorded a different result.`);
|
|
627
|
+
}
|
|
628
|
+
await atomicPrivateWrite(path.join(path.dirname(target), `${input.cycleId}.${input.role}.json`), result);
|
|
629
|
+
const updated = {
|
|
630
|
+
...session,
|
|
631
|
+
state: 'recording',
|
|
632
|
+
recordedRoles: [...new Set([...session.recordedRoles, input.role])].sort(),
|
|
633
|
+
};
|
|
634
|
+
await atomicPrivateWrite(target, updated);
|
|
635
|
+
return { recorded: input.role, session: updated };
|
|
636
|
+
}
|
|
637
|
+
const missingRoles = session.roles.filter((role) => !session.recordedRoles.includes(role));
|
|
638
|
+
if (missingRoles.length > 0) {
|
|
639
|
+
throw new OperateError('E_OPERATE_ADVISOR_FAILED', `Adapter finalize is incomplete; missing roles: ${missingRoles.join(', ')}.`);
|
|
640
|
+
}
|
|
641
|
+
const results = await Promise.all(session.recordedRoles.map((role) => readFile(path.join(path.dirname(target), `${input.cycleId}.${role}.json`), 'utf8').then((raw) => JSON.parse(raw))));
|
|
642
|
+
for (const result of results) {
|
|
643
|
+
await assertOperatingArtifact('operating-role-result', result);
|
|
644
|
+
let digestValid = true;
|
|
645
|
+
try {
|
|
646
|
+
(await loadOperatingProtocol()).validateOperatingRoleResultDigest(result);
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
digestValid = false;
|
|
650
|
+
}
|
|
651
|
+
if (result.inputDigest !== session.roleInputDigests[result.roleId] || !digestValid) {
|
|
652
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Finalized result for ${result.roleId} failed digest binding.`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
const store = new OperatingEventStore(input.projectRoot, { localRoot: input.localRoot });
|
|
656
|
+
const initial = await store.replay();
|
|
657
|
+
await withOperatingLock(input.projectRoot, {
|
|
658
|
+
projectKey: operatingProjectKey(input.projectRoot),
|
|
659
|
+
expectedEventHead: initial.eventHead,
|
|
660
|
+
currentEventHead: initial.eventHead,
|
|
661
|
+
localRoot: input.localRoot,
|
|
662
|
+
}, async (lock) => {
|
|
663
|
+
let head = initial.eventHead;
|
|
664
|
+
const existing = new Map((await persistedAdapterResults(store, input.cycleId)).map((result) => [
|
|
665
|
+
result.roleId,
|
|
666
|
+
result,
|
|
667
|
+
]));
|
|
668
|
+
for (const result of [...results].sort((left, right) => left.roleId.localeCompare(right.roleId))) {
|
|
669
|
+
const prior = existing.get(result.roleId);
|
|
670
|
+
if (prior) {
|
|
671
|
+
if (prior.resultDigest !== result.resultDigest) {
|
|
672
|
+
throw new OperateError('E_OPERATE_ADVISOR_ISOLATION', `Cycle ${input.cycleId} already committed a different ${result.roleId} result.`);
|
|
673
|
+
}
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const record = await store.putRecord('advisor-result', result, {
|
|
677
|
+
correlationId: session.idempotencyKey,
|
|
678
|
+
});
|
|
679
|
+
const event = await store.append({
|
|
680
|
+
type: 'advisory.recorded',
|
|
681
|
+
cycleId: input.cycleId,
|
|
682
|
+
entityId: `${input.cycleId}-${result.roleId}`,
|
|
683
|
+
correlationId: session.idempotencyKey,
|
|
684
|
+
evidenceRefs: result.proposals.flatMap((proposal) => proposal.evidenceRefs),
|
|
685
|
+
payload: { recordDigest: record.digest },
|
|
686
|
+
expectedHead: head.hash,
|
|
687
|
+
actor: { kind: 'runtime', id: 'operate-adapter' },
|
|
688
|
+
});
|
|
689
|
+
const next = { sequence: event.sequence, hash: event.eventHash };
|
|
690
|
+
await lock.advanceEventHead(head, next);
|
|
691
|
+
head = next;
|
|
692
|
+
}
|
|
693
|
+
await store.writeCheckpoint(await store.state());
|
|
694
|
+
});
|
|
695
|
+
const finalized = { ...session, state: 'finalized' };
|
|
696
|
+
await atomicPrivateWrite(target, finalized);
|
|
697
|
+
return { session: finalized, results };
|
|
698
|
+
}
|
|
699
|
+
//# sourceMappingURL=maintenance.js.map
|