@aopslabs/aops 0.2.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/LICENSE +19 -0
- package/NOTICE +7 -0
- package/README.md +203 -0
- package/assets/agent-assets/core/SKILL.md +49 -0
- package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
- package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
- package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
- package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
- package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
- package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
- package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
- package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
- package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
- package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
- package/assets/agent-assets/core/references/view/SKILL.md +58 -0
- package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
- package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
- package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
- package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
- package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
- package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
- package/assets/agent-assets/core/user-guides/docman.md +162 -0
- package/assets/agent-assets/core/user-guides/projectman.md +212 -0
- package/assets/agent-assets/core/user-guides/sys.md +94 -0
- package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
- package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
- package/assets/r6-remote-session-baseline.json +19 -0
- package/assets/skills/aops-install/SKILL.md +231 -0
- package/assets/skills/aops-install/agents/openai.yaml +4 -0
- package/dist/commands/activity.js +352 -0
- package/dist/commands/agent-profile.js +653 -0
- package/dist/commands/agent.js +710 -0
- package/dist/commands/agents-md.js +125 -0
- package/dist/commands/api.js +84 -0
- package/dist/commands/archive.js +873 -0
- package/dist/commands/artifact.js +535 -0
- package/dist/commands/assets.js +1414 -0
- package/dist/commands/auth/device-login.js +1 -0
- package/dist/commands/auth/legacy-export.js +177 -0
- package/dist/commands/auth/login.js +426 -0
- package/dist/commands/chat.js +1363 -0
- package/dist/commands/chatv3.js +1684 -0
- package/dist/commands/checkpoint.js +549 -0
- package/dist/commands/commercial-artifact.js +475 -0
- package/dist/commands/commercial-license.js +594 -0
- package/dist/commands/community-auth.js +62 -0
- package/dist/commands/community-cockpit.js +185 -0
- package/dist/commands/community-console.js +109 -0
- package/dist/commands/community-doctor.js +335 -0
- package/dist/commands/community-server.js +2062 -0
- package/dist/commands/community-setup.js +284 -0
- package/dist/commands/discuss.js +1448 -0
- package/dist/commands/doc.js +5219 -0
- package/dist/commands/experience.js +922 -0
- package/dist/commands/host.js +1127 -0
- package/dist/commands/init.js +118 -0
- package/dist/commands/memory.js +3129 -0
- package/dist/commands/mission.js +562 -0
- package/dist/commands/official-catalog.js +134 -0
- package/dist/commands/plan.js +84 -0
- package/dist/commands/playbook.js +415 -0
- package/dist/commands/pm/context.js +6 -0
- package/dist/commands/pm/index.js +701 -0
- package/dist/commands/pm/projectman.js +6266 -0
- package/dist/commands/project.js +1080 -0
- package/dist/commands/prompt.js +1024 -0
- package/dist/commands/repo-sync.js +1907 -0
- package/dist/commands/resource.js +527 -0
- package/dist/commands/skill.js +1211 -0
- package/dist/commands/start-disciplines.js +510 -0
- package/dist/commands/start.js +1343 -0
- package/dist/commands/target.js +309 -0
- package/dist/commands/version.js +42 -0
- package/dist/commands/view.js +1852 -0
- package/dist/launchers/cockpit.js +28 -0
- package/dist/launchers/options.js +74 -0
- package/dist/launchers/server.js +41 -0
- package/dist/lib/agent-assets/envelope.js +83 -0
- package/dist/lib/agent-assets/gateway.js +15 -0
- package/dist/lib/agent-assets/guards.js +23 -0
- package/dist/lib/agent-assets/hosted-discovery.js +148 -0
- package/dist/lib/agent-assets/hosted-package-input.js +154 -0
- package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
- package/dist/lib/agent-assets/native-fs.js +589 -0
- package/dist/lib/agent-assets/package-manifest.js +324 -0
- package/dist/lib/agent-assets/portable-path.js +67 -0
- package/dist/lib/agent-assets/release-input.js +177 -0
- package/dist/lib/agent-assets/roots.js +45 -0
- package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
- package/dist/lib/agent-assets/runtime-targets.js +50 -0
- package/dist/lib/agent-assets/store-reader.js +1212 -0
- package/dist/lib/agent-assets/store-types.js +1 -0
- package/dist/lib/agent-assets/store-writer.js +1484 -0
- package/dist/lib/agent-assets/types.js +1 -0
- package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
- package/dist/lib/commercial-final-readiness.js +504 -0
- package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
- package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
- package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
- package/dist/lib/community-client-contract.js +164 -0
- package/dist/lib/community-cockpit-lifecycle.js +601 -0
- package/dist/lib/community-command-abort.js +59 -0
- package/dist/lib/community-diagnostic.js +237 -0
- package/dist/lib/community-docker-adapter.js +498 -0
- package/dist/lib/community-home.js +31 -0
- package/dist/lib/community-instance-contract.js +148 -0
- package/dist/lib/community-instance-layout.js +97 -0
- package/dist/lib/community-lifecycle.js +1631 -0
- package/dist/lib/community-migration-snapshot.js +770 -0
- package/dist/lib/community-native-application-recovery.js +425 -0
- package/dist/lib/community-native-child.js +240 -0
- package/dist/lib/community-native-database-recovery.js +456 -0
- package/dist/lib/community-native-lifecycle.js +2458 -0
- package/dist/lib/community-native-migration.js +339 -0
- package/dist/lib/community-native-postgres.js +596 -0
- package/dist/lib/community-operation-journal.js +773 -0
- package/dist/lib/community-operation-lock.js +596 -0
- package/dist/lib/community-release-resolver.js +291 -0
- package/dist/lib/community-release-verifier.js +475 -0
- package/dist/lib/community-repo-discovery.js +252 -0
- package/dist/lib/community-setup-server-env.js +203 -0
- package/dist/lib/external-url.js +110 -0
- package/dist/lib/official-catalog-gateway.js +108 -0
- package/dist/lib/official-catalog.js +364 -0
- package/dist/lib/server-bootstrap.js +240 -0
- package/dist/lib/setup-agent-assets-bridge.js +176 -0
- package/dist/lib/setup-agent-assets-release.js +52 -0
- package/dist/lib/setup-external-postgres.js +108 -0
- package/dist/lib/setup-init-orchestrator.js +990 -0
- package/dist/lib/setup-install-guide.js +52 -0
- package/dist/lib/setup-installer-release.js +47 -0
- package/dist/lib/setup-local-postgres.js +329 -0
- package/dist/lib/setup-official-catalog-bridge.js +148 -0
- package/dist/lib/setup-readiness.js +888 -0
- package/dist/lib/tui-launcher.js +59 -0
- package/dist/main.js +150 -0
- package/dist/seeds/agents-md.js +54 -0
- package/dist/utils/agent-gateway.js +275 -0
- package/dist/utils/agents-md.js +281 -0
- package/dist/utils/api.js +255 -0
- package/dist/utils/chatv3-session-store.js +185 -0
- package/dist/utils/command.js +67 -0
- package/dist/utils/community-host-runtime.js +376 -0
- package/dist/utils/config.js +1015 -0
- package/dist/utils/discussion-workspace.js +552 -0
- package/dist/utils/experience-workspace.js +326 -0
- package/dist/utils/guide-paths.js +67 -0
- package/dist/utils/hosted-sugar.js +56 -0
- package/dist/utils/hosted-workspace.js +229 -0
- package/dist/utils/memory-workspace.js +537 -0
- package/dist/utils/operator-runtime.js +199 -0
- package/dist/utils/password-crypto.js +39 -0
- package/dist/utils/playbook-workspace.js +479 -0
- package/dist/utils/project-context.js +255 -0
- package/dist/utils/prompts.js +70 -0
- package/dist/utils/refresh-race.js +28 -0
- package/dist/utils/repo-config.js +109 -0
- package/dist/utils/repo-first-projectman.js +194 -0
- package/dist/utils/repo-first-storage.js +13 -0
- package/dist/utils/repo-first-view.js +305 -0
- package/dist/utils/session-guidance.js +278 -0
- package/dist/utils/session-state.js +137 -0
- package/dist/utils/table.js +39 -0
- package/dist/utils/transient-fs.js +49 -0
- package/dist/utils/workspace-root.js +1 -0
- package/launchers/aops-cockpit.ps1 +15 -0
- package/launchers/aops-cockpit.sh +16 -0
- package/launchers/aops-server.ps1 +15 -0
- package/launchers/aops-server.sh +16 -0
- package/native/tui/win32-x64/aops-tui.exe +0 -0
- package/package.json +81 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { closeSync, constants, fstatSync, fsyncSync, ftruncateSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, writeSync, } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isCommunityDiagnosticV1, } from './community-diagnostic.js';
|
|
5
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
6
|
+
const SHA256 = /^sha256:[a-f0-9]{64}$/;
|
|
7
|
+
const STEP = /^[a-z0-9][a-z0-9-]{0,95}$/;
|
|
8
|
+
const MAX_FRAME_BYTES = 65_536;
|
|
9
|
+
const MAX_JOURNAL_BYTES = 1_048_576;
|
|
10
|
+
const ZERO_HASH = `sha256:${'0'.repeat(64)}`;
|
|
11
|
+
function digest(content) {
|
|
12
|
+
return `sha256:${createHash('sha256').update(content).digest('hex')}`;
|
|
13
|
+
}
|
|
14
|
+
function isErrno(error, code) {
|
|
15
|
+
return error?.code === code;
|
|
16
|
+
}
|
|
17
|
+
function samePath(left, right) {
|
|
18
|
+
return path.relative(left, right) === '' && path.relative(right, left) === '';
|
|
19
|
+
}
|
|
20
|
+
function tryLstat(candidate) {
|
|
21
|
+
try {
|
|
22
|
+
return lstatSync(candidate);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (isErrno(error, 'ENOENT'))
|
|
26
|
+
return undefined;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function ensureCanonicalDirectoryChain(target) {
|
|
31
|
+
const resolved = path.resolve(target);
|
|
32
|
+
const missing = [];
|
|
33
|
+
let ancestor = resolved;
|
|
34
|
+
let stat = tryLstat(ancestor);
|
|
35
|
+
while (!stat) {
|
|
36
|
+
const parent = path.dirname(ancestor);
|
|
37
|
+
if (samePath(parent, ancestor))
|
|
38
|
+
throw new Error('community_operation_journal_parent_missing');
|
|
39
|
+
missing.unshift(path.basename(ancestor));
|
|
40
|
+
ancestor = parent;
|
|
41
|
+
stat = tryLstat(ancestor);
|
|
42
|
+
}
|
|
43
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || !samePath(ancestor, realpathSync.native(ancestor))) {
|
|
44
|
+
throw new Error('community_operation_journal_parent_unsafe');
|
|
45
|
+
}
|
|
46
|
+
let current = ancestor;
|
|
47
|
+
for (const segment of missing) {
|
|
48
|
+
const next = path.join(current, segment);
|
|
49
|
+
if (!samePath(path.dirname(next), current))
|
|
50
|
+
throw new Error('community_operation_journal_path_escape');
|
|
51
|
+
try {
|
|
52
|
+
mkdirSync(next, { mode: 0o700 });
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (!isErrno(error, 'EEXIST'))
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
const nextStat = lstatSync(next);
|
|
59
|
+
if (nextStat.isSymbolicLink() || !nextStat.isDirectory() ||
|
|
60
|
+
!samePath(next, realpathSync.native(next)) || !samePath(path.dirname(next), current)) {
|
|
61
|
+
throw new Error('community_operation_journal_directory_unsafe');
|
|
62
|
+
}
|
|
63
|
+
current = next;
|
|
64
|
+
}
|
|
65
|
+
return current;
|
|
66
|
+
}
|
|
67
|
+
function assertCanonicalJournalDirectory(target) {
|
|
68
|
+
const resolved = path.resolve(target);
|
|
69
|
+
const stat = lstatSync(resolved);
|
|
70
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() || !samePath(resolved, realpathSync.native(resolved))) {
|
|
71
|
+
throw new Error('community_operation_journal_directory_unsafe');
|
|
72
|
+
}
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
75
|
+
function sameIdentity(left, right) {
|
|
76
|
+
return String(left.dev) === String(right.dev) && String(left.ino) === String(right.ino);
|
|
77
|
+
}
|
|
78
|
+
function readBoundRegularFile(filePath, maxBytes, errorCode) {
|
|
79
|
+
const resolved = path.resolve(filePath);
|
|
80
|
+
const parent = path.dirname(resolved);
|
|
81
|
+
const parentStat = lstatSync(parent, { bigint: true });
|
|
82
|
+
if (parentStat.isSymbolicLink() || !parentStat.isDirectory() || !samePath(parent, realpathSync.native(parent))) {
|
|
83
|
+
throw new Error(errorCode);
|
|
84
|
+
}
|
|
85
|
+
const visibleBefore = lstatSync(resolved, { bigint: true });
|
|
86
|
+
if (visibleBefore.isSymbolicLink() || !visibleBefore.isFile() || visibleBefore.nlink !== 1n ||
|
|
87
|
+
visibleBefore.size > BigInt(maxBytes) || !samePath(resolved, realpathSync.native(resolved))) {
|
|
88
|
+
throw new Error(errorCode);
|
|
89
|
+
}
|
|
90
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
91
|
+
const fd = openSync(resolved, constants.O_RDONLY | noFollow);
|
|
92
|
+
try {
|
|
93
|
+
const heldBefore = fstatSync(fd, { bigint: true });
|
|
94
|
+
if (!heldBefore.isFile() || heldBefore.nlink !== 1n || heldBefore.size !== visibleBefore.size ||
|
|
95
|
+
!sameIdentity(heldBefore, visibleBefore)) {
|
|
96
|
+
throw new Error(errorCode);
|
|
97
|
+
}
|
|
98
|
+
const byteLength = Number(heldBefore.size);
|
|
99
|
+
if (!Number.isSafeInteger(byteLength))
|
|
100
|
+
throw new Error(errorCode);
|
|
101
|
+
const content = Buffer.alloc(byteLength);
|
|
102
|
+
let offset = 0;
|
|
103
|
+
while (offset < content.byteLength) {
|
|
104
|
+
const count = readSync(fd, content, offset, content.byteLength - offset, offset);
|
|
105
|
+
if (count <= 0)
|
|
106
|
+
throw new Error(errorCode);
|
|
107
|
+
offset += count;
|
|
108
|
+
}
|
|
109
|
+
const heldAfter = fstatSync(fd, { bigint: true });
|
|
110
|
+
const visibleAfter = lstatSync(resolved, { bigint: true });
|
|
111
|
+
const parentAfter = lstatSync(parent, { bigint: true });
|
|
112
|
+
if (!heldAfter.isFile() || heldAfter.nlink !== 1n || heldAfter.size !== heldBefore.size ||
|
|
113
|
+
heldAfter.mtimeNs !== heldBefore.mtimeNs || heldAfter.ctimeNs !== heldBefore.ctimeNs ||
|
|
114
|
+
!sameIdentity(heldAfter, heldBefore) || visibleAfter.isSymbolicLink() || !visibleAfter.isFile() ||
|
|
115
|
+
visibleAfter.nlink !== 1n || visibleAfter.size !== heldBefore.size ||
|
|
116
|
+
visibleAfter.mtimeNs !== heldBefore.mtimeNs || visibleAfter.ctimeNs !== heldBefore.ctimeNs ||
|
|
117
|
+
!sameIdentity(visibleAfter, heldBefore) ||
|
|
118
|
+
!parentAfter.isDirectory() || parentAfter.isSymbolicLink() || !sameIdentity(parentAfter, parentStat) ||
|
|
119
|
+
!samePath(parent, realpathSync.native(parent))) {
|
|
120
|
+
throw new Error(errorCode);
|
|
121
|
+
}
|
|
122
|
+
return content;
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
closeSync(fd);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function digestOwnedFile(filePath) {
|
|
129
|
+
if (!tryLstat(filePath))
|
|
130
|
+
return null;
|
|
131
|
+
return digest(readBoundRegularFile(filePath, 8_388_608, 'community_operation_journal_digest_source_unsafe'));
|
|
132
|
+
}
|
|
133
|
+
export function captureCommunityOperationDigests(paths) {
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
state: digestOwnedFile(paths.statePath),
|
|
136
|
+
env: digestOwnedFile(paths.envPath),
|
|
137
|
+
ledger: digestOwnedFile(paths.ledgerPath),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function assertDigestSet(value) {
|
|
141
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) ||
|
|
142
|
+
JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(['env', 'ledger', 'state'])) {
|
|
143
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
144
|
+
}
|
|
145
|
+
for (const entry of Object.values(value)) {
|
|
146
|
+
if (entry !== null && (typeof entry !== 'string' || !SHA256.test(entry))) {
|
|
147
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function assertIso(value) {
|
|
152
|
+
if (typeof value !== 'string' || new Date(value).toISOString() !== value) {
|
|
153
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function assertInstalledRelease(value) {
|
|
157
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
158
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
159
|
+
}
|
|
160
|
+
const release = value;
|
|
161
|
+
const keys = [
|
|
162
|
+
'composePath', 'composeSha256', 'imageIndexDigest', 'imageRef', 'manifestPath', 'manifestSha256',
|
|
163
|
+
'migrationSetDigest', 'migrationTags', 'releaseVersion',
|
|
164
|
+
];
|
|
165
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(keys) ||
|
|
166
|
+
typeof release.releaseVersion !== 'string' || release.releaseVersion.length < 1 || release.releaseVersion.length > 128 ||
|
|
167
|
+
typeof release.imageRef !== 'string' || !/@sha256:[a-f0-9]{64}$/.test(release.imageRef) ||
|
|
168
|
+
!SHA256.test(release.imageIndexDigest) || !SHA256.test(release.manifestSha256) ||
|
|
169
|
+
!SHA256.test(release.composeSha256) || !SHA256.test(release.migrationSetDigest) ||
|
|
170
|
+
!Array.isArray(release.migrationTags) || release.migrationTags.some((tag) => typeof tag !== 'string' || tag.length > 128) ||
|
|
171
|
+
typeof release.manifestPath !== 'string' || !path.isAbsolute(release.manifestPath) ||
|
|
172
|
+
typeof release.composePath !== 'string' || !path.isAbsolute(release.composePath)) {
|
|
173
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function assertSourceState(value) {
|
|
177
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
178
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
179
|
+
}
|
|
180
|
+
const state = value;
|
|
181
|
+
const keys = [
|
|
182
|
+
'activeRelease', 'composeProjectName', 'createdAt', 'installId', 'instanceName', 'lastSuccessfulUpdateId',
|
|
183
|
+
'postgresVolumeName', 'previousRelease', 'schemaVersion', 'updatedAt',
|
|
184
|
+
];
|
|
185
|
+
if (JSON.stringify(Object.keys(value).sort()) !== JSON.stringify(keys) || state.schemaVersion !== 1 ||
|
|
186
|
+
!/^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/.test(state.instanceName) || !UUID.test(state.installId) ||
|
|
187
|
+
state.composeProjectName !== `aops-community-${state.installId.replace(/-/g, '').slice(0, 12)}` ||
|
|
188
|
+
typeof state.postgresVolumeName !== 'string' || !/^[a-z0-9][a-z0-9-]{0,179}$/.test(state.postgresVolumeName) ||
|
|
189
|
+
(state.lastSuccessfulUpdateId !== null && !UUID.test(state.lastSuccessfulUpdateId))) {
|
|
190
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
191
|
+
}
|
|
192
|
+
assertIso(state.createdAt);
|
|
193
|
+
assertIso(state.updatedAt);
|
|
194
|
+
assertInstalledRelease(state.activeRelease);
|
|
195
|
+
if (state.previousRelease !== null)
|
|
196
|
+
assertInstalledRelease(state.previousRelease);
|
|
197
|
+
}
|
|
198
|
+
function deepFreeze(value, seen = new WeakSet()) {
|
|
199
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
200
|
+
if (seen.has(value))
|
|
201
|
+
return value;
|
|
202
|
+
seen.add(value);
|
|
203
|
+
for (const nested of Object.values(value))
|
|
204
|
+
deepFreeze(nested, seen);
|
|
205
|
+
Object.freeze(value);
|
|
206
|
+
}
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
function assertRecord(value) {
|
|
210
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
211
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
212
|
+
}
|
|
213
|
+
const record = value;
|
|
214
|
+
const allowedKeys = [
|
|
215
|
+
'createdAt', 'failure', 'id', 'operation', 'outcome', 'permittedActions', 'phase', 'postDigests', 'preDigests',
|
|
216
|
+
'receipt', 'runtimeMode', 'schemaVersion', 'sequence', 'sourceState', 'status', 'step', 'updatedAt',
|
|
217
|
+
];
|
|
218
|
+
if (Object.keys(value).some((key) => !allowedKeys.includes(key)) || record.schemaVersion !== 1 ||
|
|
219
|
+
!['oci', 'native'].includes(String(record.runtimeMode)) ||
|
|
220
|
+
typeof record.id !== 'string' || !UUID.test(record.id) ||
|
|
221
|
+
!['setup', 'update', 'recover', 'rollback', 'start', 'stop', 'restart', 'backup', 'restore', 'reset'].includes(String(record.operation)) ||
|
|
222
|
+
!['running', 'aborted', 'failed', 'succeeded', 'reconciled'].includes(String(record.status)) ||
|
|
223
|
+
!['prepared', 'side-effect-before', 'side-effect-after', 'promotion-before', 'promotion-after', 'terminal'].includes(String(record.phase)) ||
|
|
224
|
+
typeof record.step !== 'string' || !STEP.test(record.step) ||
|
|
225
|
+
!Number.isSafeInteger(record.sequence) || Number(record.sequence) < 0 ||
|
|
226
|
+
!record.receipt || typeof record.receipt !== 'object' ||
|
|
227
|
+
!Array.isArray(record.permittedActions) || record.permittedActions.some((action) => !['acknowledge-no-side-effect', 'acknowledge-native-runtime-state', 'restore-source-runtime',
|
|
228
|
+
'acknowledge-artifact-preserved', 'acknowledge-partial-install-preserved',
|
|
229
|
+
'complete-reset-preserving-volumes', 'complete-native-reset-preserving-volume'].includes(action))) {
|
|
230
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
231
|
+
}
|
|
232
|
+
assertIso(record.createdAt);
|
|
233
|
+
assertIso(record.updatedAt);
|
|
234
|
+
assertDigestSet(record.preDigests);
|
|
235
|
+
if (record.postDigests !== null)
|
|
236
|
+
assertDigestSet(record.postDigests);
|
|
237
|
+
if (record.receipt.operation !== record.operation || record.receipt.schemaVersion !== 2 ||
|
|
238
|
+
!Number.isSafeInteger(record.receipt.pid) || Number(record.receipt.pid) <= 0 ||
|
|
239
|
+
typeof record.receipt.ownerTokenSha256 !== 'string' || !SHA256.test(record.receipt.ownerTokenSha256) ||
|
|
240
|
+
typeof record.receipt.processStartIdentity !== 'string' || !SHA256.test(record.receipt.processStartIdentity)) {
|
|
241
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
242
|
+
}
|
|
243
|
+
assertIso(record.receipt.startedAt);
|
|
244
|
+
if (record.runtimeMode === 'native' && record.sourceState !== null) {
|
|
245
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
246
|
+
}
|
|
247
|
+
if (record.runtimeMode === 'oci' && record.sourceState !== null)
|
|
248
|
+
assertSourceState(record.sourceState);
|
|
249
|
+
if (record.outcome !== undefined && !['aborted', 'operation-failed', 'completed', 'operator-reconciled'].includes(record.outcome)) {
|
|
250
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
251
|
+
}
|
|
252
|
+
if (record.failure !== undefined && !isCommunityDiagnosticV1(record.failure)) {
|
|
253
|
+
throw new Error('community_operation_journal_record_invalid');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function frameHash(sequence, prevHash, record) {
|
|
257
|
+
return digest(`${sequence}\0${prevHash}\0${JSON.stringify(record)}`);
|
|
258
|
+
}
|
|
259
|
+
function encodeFrame(record, prevHash) {
|
|
260
|
+
const recordHash = frameHash(record.sequence, prevHash, record);
|
|
261
|
+
const body = Buffer.from(JSON.stringify({ sequence: record.sequence, prevHash, recordHash, record }), 'utf8');
|
|
262
|
+
if (body.byteLength > MAX_FRAME_BYTES)
|
|
263
|
+
throw new Error('community_operation_journal_frame_too_large');
|
|
264
|
+
const content = Buffer.concat([Buffer.from(body.byteLength.toString(16).padStart(8, '0') + ':', 'ascii'), body, Buffer.from('\n')]);
|
|
265
|
+
return { content, hash: recordHash };
|
|
266
|
+
}
|
|
267
|
+
function sameJson(left, right) {
|
|
268
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
269
|
+
}
|
|
270
|
+
function assertTerminalRecord(record) {
|
|
271
|
+
const expected = record.status === 'succeeded'
|
|
272
|
+
? { step: 'succeeded', outcome: 'completed', post: true }
|
|
273
|
+
: record.status === 'reconciled'
|
|
274
|
+
? { step: 'reconciled', outcome: 'operator-reconciled', post: true }
|
|
275
|
+
: record.status === 'aborted'
|
|
276
|
+
? { step: 'aborted', outcome: 'aborted', post: false }
|
|
277
|
+
: record.status === 'failed'
|
|
278
|
+
? { step: 'failed', outcome: 'operation-failed', post: false }
|
|
279
|
+
: undefined;
|
|
280
|
+
if (!expected || record.phase !== 'terminal' || record.step !== expected.step ||
|
|
281
|
+
record.outcome !== expected.outcome || (record.postDigests !== null) !== expected.post ||
|
|
282
|
+
(record.failure !== undefined && !['failed', 'reconciled'].includes(record.status))) {
|
|
283
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function assertInitialRecord(record) {
|
|
287
|
+
if (record.sequence !== 0 || record.status !== 'running' || record.phase !== 'prepared' ||
|
|
288
|
+
record.step !== 'prepared' || record.outcome !== undefined || record.failure !== undefined || record.postDigests !== null ||
|
|
289
|
+
record.updatedAt !== record.createdAt ||
|
|
290
|
+
!sameJson(record.permittedActions, permittedActions(record.operation, record.sourceState, record.runtimeMode))) {
|
|
291
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function assertSemanticTransition(previous, next) {
|
|
295
|
+
for (const key of ['id', 'operation', 'runtimeMode', 'createdAt', 'receipt', 'preDigests', 'sourceState', 'permittedActions']) {
|
|
296
|
+
if (!sameJson(previous[key], next[key]))
|
|
297
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
298
|
+
}
|
|
299
|
+
if (next.sequence !== previous.sequence + 1 || Date.parse(next.updatedAt) < Date.parse(previous.updatedAt)) {
|
|
300
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
301
|
+
}
|
|
302
|
+
if (previous.status === 'succeeded' || previous.status === 'reconciled') {
|
|
303
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
304
|
+
}
|
|
305
|
+
if (next.status === 'reconciled') {
|
|
306
|
+
if (!['running', 'aborted', 'failed'].includes(previous.status)) {
|
|
307
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
308
|
+
}
|
|
309
|
+
assertTerminalRecord(next);
|
|
310
|
+
if (!sameJson(previous.failure, next.failure)) {
|
|
311
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
312
|
+
}
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (previous.failure !== undefined || (next.failure !== undefined && next.status !== 'failed')) {
|
|
316
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
317
|
+
}
|
|
318
|
+
if (previous.status !== 'running')
|
|
319
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
320
|
+
if (next.status !== 'running') {
|
|
321
|
+
assertTerminalRecord(next);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (next.outcome !== undefined || next.postDigests !== null || next.phase === 'terminal') {
|
|
325
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
326
|
+
}
|
|
327
|
+
const valid = ((previous.phase === 'prepared' || previous.phase === 'side-effect-after' || previous.phase === 'promotion-after') &&
|
|
328
|
+
(next.phase === 'side-effect-before' || next.phase === 'promotion-before')) || (previous.phase === 'side-effect-before' && next.phase === 'side-effect-after' && previous.step === next.step) ||
|
|
329
|
+
(previous.phase === 'promotion-before' && next.phase === 'promotion-after' && previous.step === next.step);
|
|
330
|
+
if (!valid)
|
|
331
|
+
throw new Error('community_operation_journal_semantic_chain_invalid');
|
|
332
|
+
}
|
|
333
|
+
function parseJournalBuffer(content, journalPath) {
|
|
334
|
+
if (content.byteLength === 0 || content.byteLength > MAX_JOURNAL_BYTES) {
|
|
335
|
+
throw new Error('community_operation_journal_invalid');
|
|
336
|
+
}
|
|
337
|
+
let offset = 0;
|
|
338
|
+
let validBytes = 0;
|
|
339
|
+
let expectedSequence = 0;
|
|
340
|
+
let expectedPrevHash = ZERO_HASH;
|
|
341
|
+
let latest;
|
|
342
|
+
let previous;
|
|
343
|
+
let lastRunning;
|
|
344
|
+
let integrity = 'complete';
|
|
345
|
+
while (offset < content.byteLength) {
|
|
346
|
+
if (content.byteLength - offset < 9) {
|
|
347
|
+
integrity = 'partial-tail';
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
const header = content.subarray(offset, offset + 9).toString('ascii');
|
|
351
|
+
if (!/^[a-f0-9]{8}:$/.test(header)) {
|
|
352
|
+
integrity = 'corrupt-tail';
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
const length = Number.parseInt(header.slice(0, 8), 16);
|
|
356
|
+
if (length < 2 || length > MAX_FRAME_BYTES) {
|
|
357
|
+
integrity = 'corrupt-tail';
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
const frameEnd = offset + 9 + length;
|
|
361
|
+
if (frameEnd >= content.byteLength) {
|
|
362
|
+
integrity = 'partial-tail';
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
if (content[frameEnd] !== 0x0a) {
|
|
366
|
+
integrity = 'corrupt-tail';
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
let frame;
|
|
370
|
+
try {
|
|
371
|
+
frame = JSON.parse(content.subarray(offset + 9, frameEnd).toString('utf8'));
|
|
372
|
+
assertRecord(frame.record);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
integrity = 'corrupt-tail';
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
if (frame.sequence !== expectedSequence || frame.record.sequence !== expectedSequence ||
|
|
379
|
+
frame.prevHash !== expectedPrevHash || frame.recordHash !== frameHash(frame.sequence, frame.prevHash, frame.record)) {
|
|
380
|
+
integrity = 'corrupt-tail';
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
try {
|
|
384
|
+
if (previous)
|
|
385
|
+
assertSemanticTransition(previous, frame.record);
|
|
386
|
+
else
|
|
387
|
+
assertInitialRecord(frame.record);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
integrity = 'corrupt-tail';
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
latest = deepFreeze(frame.record);
|
|
394
|
+
previous = latest;
|
|
395
|
+
if (latest.status === 'running')
|
|
396
|
+
lastRunning = latest;
|
|
397
|
+
expectedPrevHash = frame.recordHash;
|
|
398
|
+
expectedSequence += 1;
|
|
399
|
+
offset = frameEnd + 1;
|
|
400
|
+
validBytes = offset;
|
|
401
|
+
}
|
|
402
|
+
if (!latest || !lastRunning || path.basename(journalPath) !== `${latest.id}.json`) {
|
|
403
|
+
throw new Error('community_operation_journal_invalid');
|
|
404
|
+
}
|
|
405
|
+
return Object.freeze({
|
|
406
|
+
path: journalPath,
|
|
407
|
+
integrity,
|
|
408
|
+
validBytes,
|
|
409
|
+
fileSha256: digest(content),
|
|
410
|
+
lastHash: expectedPrevHash,
|
|
411
|
+
lastRunningPhase: lastRunning.phase,
|
|
412
|
+
lastRunningStep: lastRunning.step,
|
|
413
|
+
record: latest,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function assertJournalFile(pathname, expectedParent) {
|
|
417
|
+
const stat = lstatSync(pathname);
|
|
418
|
+
if (stat.isSymbolicLink() || !stat.isFile() || stat.nlink !== 1 || stat.size > MAX_JOURNAL_BYTES ||
|
|
419
|
+
!samePath(path.dirname(path.resolve(pathname)), expectedParent) || !samePath(path.resolve(pathname), realpathSync.native(pathname))) {
|
|
420
|
+
throw new Error('community_operation_journal_file_unsafe');
|
|
421
|
+
}
|
|
422
|
+
return stat;
|
|
423
|
+
}
|
|
424
|
+
export function inspectCommunityOperationJournalFile(paths, operationId) {
|
|
425
|
+
if (!UUID.test(operationId))
|
|
426
|
+
throw new Error('community_operation_journal_id_invalid');
|
|
427
|
+
const journalRoot = assertCanonicalJournalDirectory(paths.operationJournalRoot);
|
|
428
|
+
const rootBefore = lstatSync(journalRoot);
|
|
429
|
+
const journalPath = path.join(journalRoot, `${operationId}.json`);
|
|
430
|
+
assertJournalFile(journalPath, journalRoot);
|
|
431
|
+
const inspection = parseJournalBuffer(readBoundRegularFile(journalPath, MAX_JOURNAL_BYTES, 'community_operation_journal_file_unsafe'), journalPath);
|
|
432
|
+
const rootAfter = lstatSync(journalRoot);
|
|
433
|
+
if (!rootAfter.isDirectory() || rootAfter.isSymbolicLink() || !sameIdentity(rootBefore, rootAfter) ||
|
|
434
|
+
!samePath(journalRoot, realpathSync.native(journalRoot))) {
|
|
435
|
+
throw new Error('community_operation_journal_directory_unsafe');
|
|
436
|
+
}
|
|
437
|
+
return inspection;
|
|
438
|
+
}
|
|
439
|
+
function permittedActions(operation, sourceState, runtimeMode) {
|
|
440
|
+
if (runtimeMode === 'native') {
|
|
441
|
+
return ['acknowledge-no-side-effect', 'acknowledge-native-runtime-state', 'complete-native-reset-preserving-volume'];
|
|
442
|
+
}
|
|
443
|
+
if (operation === 'setup') {
|
|
444
|
+
return sourceState
|
|
445
|
+
? ['acknowledge-no-side-effect', 'restore-source-runtime']
|
|
446
|
+
: ['acknowledge-no-side-effect', 'acknowledge-partial-install-preserved', 'complete-reset-preserving-volumes'];
|
|
447
|
+
}
|
|
448
|
+
if (operation === 'backup')
|
|
449
|
+
return ['acknowledge-no-side-effect', 'acknowledge-artifact-preserved'];
|
|
450
|
+
if (operation === 'reset')
|
|
451
|
+
return ['acknowledge-no-side-effect', 'complete-reset-preserving-volumes'];
|
|
452
|
+
return sourceState ? ['acknowledge-no-side-effect', 'restore-source-runtime'] : ['acknowledge-no-side-effect'];
|
|
453
|
+
}
|
|
454
|
+
function fsyncDirectoryEntry(directory) {
|
|
455
|
+
let fd;
|
|
456
|
+
try {
|
|
457
|
+
fd = openSync(directory, constants.O_RDONLY);
|
|
458
|
+
fsyncSync(fd);
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
const code = String(error?.code ?? '');
|
|
462
|
+
if (process.platform !== 'win32' || !['EACCES', 'EINVAL', 'EPERM'].includes(code))
|
|
463
|
+
throw error;
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
if (fd !== undefined)
|
|
467
|
+
closeSync(fd);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
function createHandle(params) {
|
|
471
|
+
let current = params.initial;
|
|
472
|
+
let currentHash = params.initialHash;
|
|
473
|
+
let currentSize = params.initialSize;
|
|
474
|
+
let closed = false;
|
|
475
|
+
const original = fstatSync(params.fd, { bigint: true });
|
|
476
|
+
let baseline = original;
|
|
477
|
+
const rootOriginal = params.rootOriginal;
|
|
478
|
+
const journalParent = path.dirname(params.journalRoot);
|
|
479
|
+
const parentOriginal = params.parentOriginal;
|
|
480
|
+
const assertOwned = () => {
|
|
481
|
+
if (closed)
|
|
482
|
+
throw new Error('community_operation_journal_handle_closed');
|
|
483
|
+
assertJournalFile(params.journalPath, params.journalRoot);
|
|
484
|
+
const held = fstatSync(params.fd, { bigint: true });
|
|
485
|
+
const visible = lstatSync(params.journalPath, { bigint: true });
|
|
486
|
+
const root = lstatSync(params.journalRoot, { bigint: true });
|
|
487
|
+
const parent = lstatSync(journalParent, { bigint: true });
|
|
488
|
+
if (!held.isFile() || held.nlink !== 1n || !sameIdentity(held, original) || held.size !== BigInt(currentSize) ||
|
|
489
|
+
held.mtimeNs !== baseline.mtimeNs || held.ctimeNs !== baseline.ctimeNs ||
|
|
490
|
+
visible.isSymbolicLink() || !visible.isFile() || visible.nlink !== 1n || !sameIdentity(visible, original) ||
|
|
491
|
+
visible.size !== held.size || visible.mtimeNs !== held.mtimeNs || visible.ctimeNs !== held.ctimeNs ||
|
|
492
|
+
root.isSymbolicLink() || !root.isDirectory() || !sameIdentity(root, rootOriginal) ||
|
|
493
|
+
root.mtimeNs !== rootOriginal.mtimeNs || root.ctimeNs !== rootOriginal.ctimeNs ||
|
|
494
|
+
parent.isSymbolicLink() || !parent.isDirectory() || !sameIdentity(parent, parentOriginal) ||
|
|
495
|
+
!samePath(params.journalRoot, realpathSync.native(params.journalRoot))) {
|
|
496
|
+
throw new Error('community_operation_journal_path_identity_changed');
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
const append = (next) => {
|
|
500
|
+
assertOwned();
|
|
501
|
+
const encoded = encodeFrame(next, currentHash);
|
|
502
|
+
if (currentSize + encoded.content.byteLength > MAX_JOURNAL_BYTES) {
|
|
503
|
+
throw new Error('community_operation_journal_size_limit');
|
|
504
|
+
}
|
|
505
|
+
const written = writeSync(params.fd, encoded.content, 0, encoded.content.byteLength, currentSize);
|
|
506
|
+
if (written !== encoded.content.byteLength)
|
|
507
|
+
throw new Error('community_operation_journal_short_write');
|
|
508
|
+
fsyncSync(params.fd);
|
|
509
|
+
const nextSize = currentSize + written;
|
|
510
|
+
assertJournalFile(params.journalPath, params.journalRoot);
|
|
511
|
+
const heldAfter = fstatSync(params.fd, { bigint: true });
|
|
512
|
+
const visibleAfter = lstatSync(params.journalPath, { bigint: true });
|
|
513
|
+
const rootAfter = lstatSync(params.journalRoot, { bigint: true });
|
|
514
|
+
const parentAfter = lstatSync(journalParent, { bigint: true });
|
|
515
|
+
if (!heldAfter.isFile() || heldAfter.nlink !== 1n || !sameIdentity(heldAfter, original) ||
|
|
516
|
+
heldAfter.size !== BigInt(nextSize) || visibleAfter.isSymbolicLink() || !visibleAfter.isFile() ||
|
|
517
|
+
visibleAfter.nlink !== 1n || !sameIdentity(visibleAfter, heldAfter) || visibleAfter.size !== heldAfter.size ||
|
|
518
|
+
visibleAfter.mtimeNs !== heldAfter.mtimeNs || visibleAfter.ctimeNs !== heldAfter.ctimeNs ||
|
|
519
|
+
rootAfter.isSymbolicLink() || !rootAfter.isDirectory() || !sameIdentity(rootAfter, rootOriginal) ||
|
|
520
|
+
rootAfter.mtimeNs !== rootOriginal.mtimeNs || rootAfter.ctimeNs !== rootOriginal.ctimeNs) {
|
|
521
|
+
throw new Error('community_operation_journal_path_identity_changed');
|
|
522
|
+
}
|
|
523
|
+
if (parentAfter.isSymbolicLink() || !parentAfter.isDirectory() || !sameIdentity(parentAfter, parentOriginal)) {
|
|
524
|
+
throw new Error('community_operation_journal_path_identity_changed');
|
|
525
|
+
}
|
|
526
|
+
currentSize = nextSize;
|
|
527
|
+
baseline = heldAfter;
|
|
528
|
+
current = deepFreeze(next);
|
|
529
|
+
currentHash = encoded.hash;
|
|
530
|
+
assertOwned();
|
|
531
|
+
return current;
|
|
532
|
+
};
|
|
533
|
+
const transition = (phase, step) => {
|
|
534
|
+
if (!STEP.test(step) || phase === 'terminal' || current.status !== 'running') {
|
|
535
|
+
throw new Error('community_operation_journal_transition_invalid');
|
|
536
|
+
}
|
|
537
|
+
const valid = ((current.phase === 'prepared' || current.phase === 'side-effect-after' || current.phase === 'promotion-after') &&
|
|
538
|
+
(phase === 'side-effect-before' || phase === 'promotion-before')) || (current.phase === 'side-effect-before' && phase === 'side-effect-after' && current.step === step) ||
|
|
539
|
+
(current.phase === 'promotion-before' && phase === 'promotion-after' && current.step === step);
|
|
540
|
+
if (!valid)
|
|
541
|
+
throw new Error('community_operation_journal_transition_invalid');
|
|
542
|
+
return append({
|
|
543
|
+
...current,
|
|
544
|
+
phase,
|
|
545
|
+
step,
|
|
546
|
+
sequence: current.sequence + 1,
|
|
547
|
+
updatedAt: new Date().toISOString(),
|
|
548
|
+
});
|
|
549
|
+
};
|
|
550
|
+
const finish = (status, outcome, postDigests, failure) => {
|
|
551
|
+
const reconciliationAllowed = status === 'reconciled' && params.allowReconcile === true &&
|
|
552
|
+
['running', 'aborted', 'failed'].includes(current.status);
|
|
553
|
+
if ((!reconciliationAllowed && current.status !== 'running') || (status === 'reconciled' && !reconciliationAllowed) ||
|
|
554
|
+
(status === 'succeeded' && (current.phase === 'side-effect-before' || current.phase === 'promotion-before')) ||
|
|
555
|
+
((status === 'succeeded' || status === 'reconciled') && !postDigests) ||
|
|
556
|
+
(failure !== undefined && status !== 'failed')) {
|
|
557
|
+
throw new Error('community_operation_journal_transition_invalid');
|
|
558
|
+
}
|
|
559
|
+
const terminalFailure = status === 'reconciled' ? current.failure : failure;
|
|
560
|
+
return append({
|
|
561
|
+
...current,
|
|
562
|
+
status,
|
|
563
|
+
phase: 'terminal',
|
|
564
|
+
step: status,
|
|
565
|
+
sequence: current.sequence + 1,
|
|
566
|
+
updatedAt: new Date().toISOString(),
|
|
567
|
+
postDigests: status === 'succeeded' || status === 'reconciled' ? postDigests : null,
|
|
568
|
+
outcome,
|
|
569
|
+
...(terminalFailure ? { failure: terminalFailure } : {}),
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
const close = () => {
|
|
573
|
+
if (closed)
|
|
574
|
+
return;
|
|
575
|
+
closed = true;
|
|
576
|
+
closeSync(params.fd);
|
|
577
|
+
};
|
|
578
|
+
return Object.freeze({ path: params.journalPath, record: () => current, assertOwned, transition, finish, close });
|
|
579
|
+
}
|
|
580
|
+
export function createCommunityOperationJournal(params) {
|
|
581
|
+
const id = (params.createId ?? randomUUID)();
|
|
582
|
+
if (!UUID.test(id) || params.receipt.operation !== params.operation || params.receipt.schemaVersion !== 2) {
|
|
583
|
+
throw new Error('community_operation_journal_create_invalid');
|
|
584
|
+
}
|
|
585
|
+
const runtimeMode = params.runtimeMode ?? 'oci';
|
|
586
|
+
if (runtimeMode === 'native' && params.sourceState !== null) {
|
|
587
|
+
throw new Error('community_operation_journal_native_source_state_refused');
|
|
588
|
+
}
|
|
589
|
+
const sourceState = params.sourceState ? deepFreeze(structuredClone(params.sourceState)) : null;
|
|
590
|
+
if (runtimeMode === 'oci' && sourceState)
|
|
591
|
+
assertSourceState(sourceState);
|
|
592
|
+
const journalRoot = ensureCanonicalDirectoryChain(params.paths.operationJournalRoot);
|
|
593
|
+
const journalParent = path.dirname(journalRoot);
|
|
594
|
+
const rootOriginal = lstatSync(journalRoot, { bigint: true });
|
|
595
|
+
const parentOriginal = lstatSync(journalParent, { bigint: true });
|
|
596
|
+
const journalPath = path.join(journalRoot, `${id}.json`);
|
|
597
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
598
|
+
const syncFlag = typeof constants.O_SYNC === 'number' ? constants.O_SYNC : 0;
|
|
599
|
+
const fd = openSync(journalPath, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | noFollow | syncFlag, 0o600);
|
|
600
|
+
try {
|
|
601
|
+
const createdAt = (params.now ?? (() => new Date()))().toISOString();
|
|
602
|
+
const initial = deepFreeze({
|
|
603
|
+
schemaVersion: 1,
|
|
604
|
+
runtimeMode,
|
|
605
|
+
id,
|
|
606
|
+
operation: params.operation,
|
|
607
|
+
status: 'running',
|
|
608
|
+
phase: 'prepared',
|
|
609
|
+
step: 'prepared',
|
|
610
|
+
sequence: 0,
|
|
611
|
+
createdAt,
|
|
612
|
+
updatedAt: createdAt,
|
|
613
|
+
receipt: Object.freeze({ ...params.receipt }),
|
|
614
|
+
preDigests: captureCommunityOperationDigests(params.paths),
|
|
615
|
+
postDigests: null,
|
|
616
|
+
sourceState,
|
|
617
|
+
permittedActions: permittedActions(params.operation, sourceState, runtimeMode),
|
|
618
|
+
});
|
|
619
|
+
const encoded = encodeFrame(initial, ZERO_HASH);
|
|
620
|
+
const written = writeSync(fd, encoded.content, 0, encoded.content.byteLength, 0);
|
|
621
|
+
if (written !== encoded.content.byteLength)
|
|
622
|
+
throw new Error('community_operation_journal_short_write');
|
|
623
|
+
fsyncSync(fd);
|
|
624
|
+
fsyncDirectoryEntry(journalRoot);
|
|
625
|
+
const rootAfterCreate = lstatSync(journalRoot, { bigint: true });
|
|
626
|
+
const parentAfterCreate = lstatSync(journalParent, { bigint: true });
|
|
627
|
+
if (!sameIdentity(rootAfterCreate, rootOriginal) || !sameIdentity(parentAfterCreate, parentOriginal)) {
|
|
628
|
+
throw new Error('community_operation_journal_path_identity_changed');
|
|
629
|
+
}
|
|
630
|
+
const handle = createHandle({
|
|
631
|
+
fd,
|
|
632
|
+
journalPath,
|
|
633
|
+
journalRoot,
|
|
634
|
+
initial,
|
|
635
|
+
initialHash: encoded.hash,
|
|
636
|
+
initialSize: written,
|
|
637
|
+
rootOriginal: rootAfterCreate,
|
|
638
|
+
parentOriginal,
|
|
639
|
+
});
|
|
640
|
+
handle.assertOwned();
|
|
641
|
+
return handle;
|
|
642
|
+
}
|
|
643
|
+
catch (error) {
|
|
644
|
+
closeSync(fd);
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
export function openCommunityOperationJournalForReconciliation(params) {
|
|
649
|
+
if (params.confirm !== true || !UUID.test(params.operationId)) {
|
|
650
|
+
throw new Error('community_operation_journal_reconciliation_confirmation_required');
|
|
651
|
+
}
|
|
652
|
+
const journalRoot = assertCanonicalJournalDirectory(params.paths.operationJournalRoot);
|
|
653
|
+
const journalParent = path.dirname(journalRoot);
|
|
654
|
+
const rootOriginal = lstatSync(journalRoot, { bigint: true });
|
|
655
|
+
const parentOriginal = lstatSync(journalParent, { bigint: true });
|
|
656
|
+
const journalPath = path.join(journalRoot, `${params.operationId}.json`);
|
|
657
|
+
const noFollow = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
658
|
+
const syncFlag = typeof constants.O_SYNC === 'number' ? constants.O_SYNC : 0;
|
|
659
|
+
const fd = openSync(journalPath, constants.O_RDWR | noFollow | syncFlag);
|
|
660
|
+
try {
|
|
661
|
+
const heldBefore = fstatSync(fd, { bigint: true });
|
|
662
|
+
const visibleBefore = lstatSync(journalPath, { bigint: true });
|
|
663
|
+
if (!heldBefore.isFile() || heldBefore.nlink !== 1n || heldBefore.size < 1n ||
|
|
664
|
+
heldBefore.size > BigInt(MAX_JOURNAL_BYTES) || visibleBefore.isSymbolicLink() || !visibleBefore.isFile() ||
|
|
665
|
+
visibleBefore.nlink !== 1n || !sameIdentity(visibleBefore, heldBefore)) {
|
|
666
|
+
throw new Error('community_operation_journal_file_unsafe');
|
|
667
|
+
}
|
|
668
|
+
const byteLength = Number(heldBefore.size);
|
|
669
|
+
const content = Buffer.alloc(byteLength);
|
|
670
|
+
let offset = 0;
|
|
671
|
+
while (offset < byteLength) {
|
|
672
|
+
const count = readSync(fd, content, offset, byteLength - offset, offset);
|
|
673
|
+
if (count <= 0)
|
|
674
|
+
throw new Error('community_operation_journal_read_failed');
|
|
675
|
+
offset += count;
|
|
676
|
+
}
|
|
677
|
+
const heldAfter = fstatSync(fd, { bigint: true });
|
|
678
|
+
const visibleAfter = lstatSync(journalPath, { bigint: true });
|
|
679
|
+
if (!sameIdentity(heldAfter, heldBefore) || heldAfter.size !== heldBefore.size ||
|
|
680
|
+
heldAfter.mtimeNs !== heldBefore.mtimeNs || heldAfter.ctimeNs !== heldBefore.ctimeNs ||
|
|
681
|
+
!sameIdentity(visibleAfter, heldBefore) || visibleAfter.size !== heldBefore.size ||
|
|
682
|
+
visibleAfter.mtimeNs !== heldBefore.mtimeNs || visibleAfter.ctimeNs !== heldBefore.ctimeNs ||
|
|
683
|
+
!sameIdentity(lstatSync(journalRoot, { bigint: true }), rootOriginal) ||
|
|
684
|
+
!sameIdentity(lstatSync(journalParent, { bigint: true }), parentOriginal)) {
|
|
685
|
+
throw new Error('community_operation_journal_path_identity_changed');
|
|
686
|
+
}
|
|
687
|
+
const inspection = parseJournalBuffer(content, journalPath);
|
|
688
|
+
if (inspection.record.id !== params.operationId || inspection.record.operation !== params.expectedOperation ||
|
|
689
|
+
!inspection.record.permittedActions.includes(params.action) ||
|
|
690
|
+
(params.expectedSequence !== undefined && inspection.record.sequence !== params.expectedSequence) ||
|
|
691
|
+
(params.expectedLastHash !== undefined && inspection.lastHash !== params.expectedLastHash) ||
|
|
692
|
+
(params.expectedFileSha256 !== undefined && inspection.fileSha256 !== params.expectedFileSha256) ||
|
|
693
|
+
['succeeded', 'reconciled'].includes(inspection.record.status)) {
|
|
694
|
+
throw new Error('community_operation_journal_reconciliation_identity_mismatch');
|
|
695
|
+
}
|
|
696
|
+
if (inspection.validBytes !== byteLength) {
|
|
697
|
+
ftruncateSync(fd, inspection.validBytes);
|
|
698
|
+
fsyncSync(fd);
|
|
699
|
+
}
|
|
700
|
+
const handle = createHandle({
|
|
701
|
+
fd,
|
|
702
|
+
journalPath,
|
|
703
|
+
journalRoot,
|
|
704
|
+
initial: inspection.record,
|
|
705
|
+
initialHash: inspection.lastHash,
|
|
706
|
+
initialSize: inspection.validBytes,
|
|
707
|
+
allowReconcile: true,
|
|
708
|
+
rootOriginal,
|
|
709
|
+
parentOriginal,
|
|
710
|
+
});
|
|
711
|
+
handle.assertOwned();
|
|
712
|
+
return handle;
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
closeSync(fd);
|
|
716
|
+
throw error;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
export function finishCommunityOperationJournal(handle, paths, status, failure) {
|
|
720
|
+
const postDigests = status === 'succeeded' || status === 'reconciled'
|
|
721
|
+
? captureCommunityOperationDigests(paths)
|
|
722
|
+
: undefined;
|
|
723
|
+
const outcome = status === 'aborted' ? 'aborted'
|
|
724
|
+
: status === 'failed' ? 'operation-failed'
|
|
725
|
+
: status === 'reconciled' ? 'operator-reconciled'
|
|
726
|
+
: 'completed';
|
|
727
|
+
return handle.finish(status, outcome, postDigests, failure);
|
|
728
|
+
}
|
|
729
|
+
export async function runCommunityJournaledSideEffect(params) {
|
|
730
|
+
if (params.signal?.aborted)
|
|
731
|
+
throw new Error('community_operation_aborted');
|
|
732
|
+
params.handle.transition('side-effect-before', params.step);
|
|
733
|
+
params.handle.assertOwned();
|
|
734
|
+
if (params.signal?.aborted)
|
|
735
|
+
throw new Error('community_operation_aborted');
|
|
736
|
+
const result = await params.effect();
|
|
737
|
+
if (params.signal?.aborted)
|
|
738
|
+
throw new Error('community_operation_aborted');
|
|
739
|
+
params.handle.transition('side-effect-after', params.step);
|
|
740
|
+
params.handle.assertOwned();
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
export async function runCommunityJournaledPromotion(params) {
|
|
744
|
+
if (params.signal?.aborted)
|
|
745
|
+
throw new Error('community_operation_aborted');
|
|
746
|
+
params.handle.transition('promotion-before', params.step);
|
|
747
|
+
params.handle.assertOwned();
|
|
748
|
+
if (params.signal?.aborted)
|
|
749
|
+
throw new Error('community_operation_aborted');
|
|
750
|
+
const result = await params.promote();
|
|
751
|
+
if (params.signal?.aborted)
|
|
752
|
+
throw new Error('community_operation_aborted');
|
|
753
|
+
params.handle.transition('promotion-after', params.step);
|
|
754
|
+
params.handle.assertOwned();
|
|
755
|
+
return result;
|
|
756
|
+
}
|
|
757
|
+
export function assertCommunityOperationJournalFence(_paths, active) {
|
|
758
|
+
if (active) {
|
|
759
|
+
active.handle.assertOwned();
|
|
760
|
+
const activeRecord = active.handle.record();
|
|
761
|
+
const statusAllowed = active.reconciliation === true
|
|
762
|
+
? !['succeeded', 'reconciled'].includes(activeRecord.status)
|
|
763
|
+
: activeRecord.status === 'running';
|
|
764
|
+
if (!statusAllowed || activeRecord.operation !== active.operation ||
|
|
765
|
+
JSON.stringify(activeRecord.receipt) !== JSON.stringify(active.receipt)) {
|
|
766
|
+
throw new Error('community_operation_journal_active_identity_invalid');
|
|
767
|
+
}
|
|
768
|
+
active.handle.assertOwned();
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
export function digestsEqual(left, right) {
|
|
772
|
+
return left.state === right.state && left.env === right.env && left.ledger === right.ledger;
|
|
773
|
+
}
|