agentic-workflow-manager 9.7.1 → 9.9.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 +5 -1
- package/dist/src/commands/job/exec-wrapper.js +53 -3
- package/dist/src/commands/job/gate.js +18 -0
- package/dist/src/commands/job/index.js +61 -0
- package/dist/src/commands/model-policy/index.js +73 -0
- package/dist/src/commands/plan/index.js +182 -7
- package/dist/src/commands/sensors/bootstrap.js +36 -5
- package/dist/src/commands/sensors/compatibility/contract.js +19 -4
- package/dist/src/commands/sensors/compatibility/discovery.js +23 -3
- package/dist/src/commands/sensors/compatibility/live.js +18 -1
- package/dist/src/commands/sensors/compatibility/probe.js +8 -2
- package/dist/src/commands/sensors/compatibility/resolve.js +5 -2
- package/dist/src/commands/sensors/coverage/evidence.js +19 -10
- package/dist/src/commands/sensors/coverage/index.js +11 -2
- package/dist/src/commands/sensors/coverage/resolve.js +10 -0
- package/dist/src/commands/sensors/exec.js +16 -1
- package/dist/src/commands/sensors/migrate.js +62 -2
- package/dist/src/commands/sensors/prepare.js +2 -0
- package/dist/src/commands/sensors/result.js +2 -0
- package/dist/src/commands/sensors/run.js +64 -1
- package/dist/src/commands/sensors/status.js +6 -4
- package/dist/src/commands/watch/apply.js +32 -0
- package/dist/src/commands/watch/archive-unused.js +138 -0
- package/dist/src/commands/watch/generations.js +32 -9
- package/dist/src/commands/watch/index.js +108 -2
- package/dist/src/commands/watch/init.js +149 -18
- package/dist/src/commands/watch/supervisor.js +167 -13
- package/dist/src/commands/watch/teardown-driver.js +13 -13
- package/dist/src/commands/watch/track-state.js +59 -0
- package/dist/src/commands/watch/tracks.js +38 -102
- package/dist/src/core/admission/index.js +231 -0
- package/dist/src/core/admission/registry-contracts.js +290 -0
- package/dist/src/core/journal/fingerprint.js +35 -1
- package/dist/src/core/journal/paths.js +11 -0
- package/dist/src/core/journal/requests.js +1 -1
- package/dist/src/core/journal/store.js +146 -4
- package/dist/src/core/journal/types.js +57 -2
- package/dist/src/core/migration/historical-provenance.js +18 -0
- package/dist/src/core/migration/index.js +322 -0
- package/dist/src/core/model-policy/canonical.js +18 -0
- package/dist/src/core/model-policy/capabilities.js +197 -0
- package/dist/src/core/model-policy/capability-types.js +2 -0
- package/dist/src/core/model-policy/journal.js +124 -0
- package/dist/src/core/model-policy/paths.js +12 -0
- package/dist/src/core/model-policy/resolve.js +116 -0
- package/dist/src/core/model-policy/store.js +182 -0
- package/dist/src/core/model-policy/types.js +2 -0
- package/dist/src/core/model-policy/validate.js +112 -0
- package/dist/src/core/plan/identity.js +77 -0
- package/dist/src/core/plan/validate.js +191 -78
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +40 -10
- package/dist/tests/commands/job/gate.test.js +9 -0
- package/dist/tests/commands/job/routing-report-cli.test.js +46 -0
- package/dist/tests/commands/job/routing.test.js +11 -0
- package/dist/tests/commands/model-policy/index.test.js +57 -0
- package/dist/tests/commands/pin.test.js +23 -23
- package/dist/tests/commands/plan/index.test.js +682 -14
- package/dist/tests/commands/sensors/bootstrap.test.js +57 -4
- package/dist/tests/commands/sensors/compatibility/contract.test.js +21 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +19 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +14 -1
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +17 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +51 -27
- package/dist/tests/commands/sensors/coverage/resolve.test.js +51 -0
- package/dist/tests/commands/sensors/exec.test.js +112 -0
- package/dist/tests/commands/sensors/run-changed.test.js +11 -0
- package/dist/tests/commands/sensors/run.test.js +15 -2
- package/dist/tests/commands/sensors/status.test.js +3 -3
- package/dist/tests/commands/watch/apply.test.js +12 -0
- package/dist/tests/commands/watch/e2e-crash.test.js +126 -11
- package/dist/tests/commands/watch/generations.test.js +113 -4
- package/dist/tests/commands/watch/plan-lifecycle.test.js +263 -0
- package/dist/tests/commands/watch/supervisor-loop.test.js +247 -49
- package/dist/tests/commands/watch/track-finalize.test.js +20 -3
- package/dist/tests/commands/watch/track-freeze.test.js +43 -30
- package/dist/tests/commands/watch/track-state-dependency.test.js +15 -0
- package/dist/tests/commands/watch/watch-init.test.js +244 -0
- package/dist/tests/core/admission/index.test.js +140 -0
- package/dist/tests/core/journal/fingerprint.test.js +22 -0
- package/dist/tests/core/journal/store.test.js +53 -0
- package/dist/tests/core/journal/types.test.js +7 -0
- package/dist/tests/core/migration/facts.test.js +289 -0
- package/dist/tests/core/migration/historical-provenance.test.js +25 -0
- package/dist/tests/core/model-policy/capabilities.test.js +365 -0
- package/dist/tests/core/model-policy/journal.test.js +46 -0
- package/dist/tests/core/model-policy/policy.test.js +47 -0
- package/dist/tests/core/model-policy/resolve.test.js +111 -0
- package/dist/tests/core/model-policy/store.test.js +177 -0
- package/dist/tests/core/plan/validate.test.js +360 -8
- package/dist/tests/core/secure-fs/native-bridge.test.js +81 -4
- package/dist/tests/integration/pack-e2e.test.js +4 -1
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +7 -3
- package/dist/tests/integration/sensor-bootstrap.e2e.test.js +37 -0
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +12 -10
- package/dist/tests/providers/injection-config.test.js +14 -6
- package/dist/tests/structural/directory-fsync-is-single-source.test.js +97 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +29 -24
- package/dist/tests/structural/native-release-artifacts.test.js +30 -0
- package/package.json +4 -3
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/linux-arm64/secure_fs.node +0 -0
- package/prebuilds/linux-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.consumedRegistryContracts = consumedRegistryContracts;
|
|
7
|
+
exports.registryCompatibility = registryCompatibility;
|
|
8
|
+
exports.admitRegistryPlan = admitRegistryPlan;
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
+
const providers_1 = require("../../providers");
|
|
13
|
+
const registry_1 = require("../renderers/registry");
|
|
14
|
+
const registries_1 = require("../registries");
|
|
15
|
+
const native_bridge_1 = require("../secure-fs/native-bridge");
|
|
16
|
+
const json_1 = require("../plan/json");
|
|
17
|
+
const cli_version_1 = require("../cli-version");
|
|
18
|
+
const versioning_1 = require("../versioning");
|
|
19
|
+
const check_1 = require("../currentness/check");
|
|
20
|
+
const run_1 = require("../../commands/sensors/run");
|
|
21
|
+
const index_1 = require("./index");
|
|
22
|
+
const FRAMEWORK_CONTRACTS = ['using-awm', 'writing-plans', 'development-process', 'subagent-driven-development', 'executing-plans', 'post-implementation-qa', 'post-implementation-docs', 'harness-retro', 'finishing-a-development-branch'];
|
|
23
|
+
const MAX_CONTRACT_BYTES = 1024 * 1024;
|
|
24
|
+
const MAX_MANIFEST_BYTES = 256 * 1024;
|
|
25
|
+
const MAX_DIRECTORY_ENTRIES = 10000;
|
|
26
|
+
function unknown(message) {
|
|
27
|
+
return { provenance: 'unknown', consumedRegistryComponents: [], provenanceDiagnostic: { code: 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED', message: `${message} Repair the installed runtime artifact or registry provenance before dispatch.` } };
|
|
28
|
+
}
|
|
29
|
+
function within(root, candidate) {
|
|
30
|
+
const relative = path_1.default.relative(root, candidate);
|
|
31
|
+
return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative));
|
|
32
|
+
}
|
|
33
|
+
function physicalDirectory(directory) {
|
|
34
|
+
const physical = fs_1.default.realpathSync.native(directory);
|
|
35
|
+
if (!fs_1.default.lstatSync(physical).isDirectory())
|
|
36
|
+
throw new Error('path is not a directory');
|
|
37
|
+
const handle = fs_1.default.opendirSync(physical);
|
|
38
|
+
try {
|
|
39
|
+
let entries = 0;
|
|
40
|
+
while (handle.readSync() !== null)
|
|
41
|
+
if (++entries > MAX_DIRECTORY_ENTRIES)
|
|
42
|
+
throw new Error('directory entry limit exceeded');
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
handle.closeSync();
|
|
46
|
+
}
|
|
47
|
+
return physical;
|
|
48
|
+
}
|
|
49
|
+
function filesystemIdentity(file) {
|
|
50
|
+
const stat = fs_1.default.lstatSync(file, { bigint: true });
|
|
51
|
+
if (stat.isSymbolicLink() || stat.ino === 0n)
|
|
52
|
+
throw new Error('filesystem identity is unavailable');
|
|
53
|
+
return `${stat.dev.toString()}:${stat.ino.toString()}`;
|
|
54
|
+
}
|
|
55
|
+
/** Native descriptor bytes plus exact bigint leaf identity are observed only
|
|
56
|
+
* in memory. No source body or new journal snapshot is persisted. */
|
|
57
|
+
function contractFileIdentity(file) {
|
|
58
|
+
const before = fs_1.default.lstatSync(file, { bigint: true });
|
|
59
|
+
if (!before.isFile() || before.isSymbolicLink() || before.ino === 0n)
|
|
60
|
+
throw new Error('contract identity is unavailable');
|
|
61
|
+
const bytes = native_bridge_1.secureFs.readRegularFile(file, MAX_CONTRACT_BYTES).bytes;
|
|
62
|
+
const after = fs_1.default.lstatSync(file, { bigint: true });
|
|
63
|
+
if (!after.isFile() || after.isSymbolicLink() || before.dev !== after.dev || before.ino !== after.ino
|
|
64
|
+
|| before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs)
|
|
65
|
+
throw new Error('contract identity changed during inspection');
|
|
66
|
+
return crypto_1.default.createHash('sha256').update(`${before.dev.toString()}:${before.ino.toString()}\0`).update(bytes).digest('hex');
|
|
67
|
+
}
|
|
68
|
+
/** ENOENT proves absence only when the nearest existing parent is readable.
|
|
69
|
+
* A dangling/replaced ancestor never certifies an empty provider install. */
|
|
70
|
+
function directoryExists(directory) {
|
|
71
|
+
let current = directory;
|
|
72
|
+
while (true) {
|
|
73
|
+
try {
|
|
74
|
+
fs_1.default.lstatSync(current);
|
|
75
|
+
physicalDirectory(current);
|
|
76
|
+
return current === directory;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code !== 'ENOENT')
|
|
80
|
+
throw error;
|
|
81
|
+
const parent = path_1.default.dirname(current);
|
|
82
|
+
if (parent === current)
|
|
83
|
+
throw error;
|
|
84
|
+
// An existing dangling symlink causes realpath ENOENT; do not
|
|
85
|
+
// continue above it as though the requested install were absent.
|
|
86
|
+
try {
|
|
87
|
+
if (fs_1.default.lstatSync(current).isSymbolicLink())
|
|
88
|
+
throw new Error('dangling directory ancestor');
|
|
89
|
+
}
|
|
90
|
+
catch (inspection) {
|
|
91
|
+
if (inspection.code !== 'ENOENT')
|
|
92
|
+
throw inspection;
|
|
93
|
+
}
|
|
94
|
+
current = parent;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Physical sources AND actual provider artifacts define consumption. Copied
|
|
99
|
+
* rendered artifacts have no owner proof; a basename is never provenance. */
|
|
100
|
+
function consumedRegistryContracts(report, cwd, registries, target) {
|
|
101
|
+
if (report.state !== 'valid')
|
|
102
|
+
return unknown('A validated compact plan is required.');
|
|
103
|
+
if (!Array.isArray(registries) || registries.length > 256)
|
|
104
|
+
return unknown('Registry inventory is invalid or unbounded.');
|
|
105
|
+
let root;
|
|
106
|
+
try {
|
|
107
|
+
root = physicalDirectory(path_1.default.resolve(cwd));
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return unknown('Project root provenance is unavailable.');
|
|
111
|
+
}
|
|
112
|
+
const physicalRegistries = [];
|
|
113
|
+
for (const registry of registries) {
|
|
114
|
+
try {
|
|
115
|
+
if (!registry || typeof registry.name !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/.test(registry.name)
|
|
116
|
+
|| typeof registry.contentRoot !== 'string' || registry.contentRoot.length > 4096
|
|
117
|
+
|| typeof registry.remote !== 'string' || registry.remote.length > 4096)
|
|
118
|
+
throw new Error('invalid registry identity');
|
|
119
|
+
const physicalRoot = physicalDirectory(registry.contentRoot);
|
|
120
|
+
physicalRegistries.push({ name: registry.name, root: physicalRoot, identity: filesystemIdentity(physicalRoot), remote: registry.remote });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
const name = typeof registry?.name === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/.test(registry.name) ? registry.name : 'unknown';
|
|
124
|
+
return unknown(`Registry ${name} root cannot be inspected.`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const consumed = new Set();
|
|
128
|
+
const identities = [];
|
|
129
|
+
for (const source of report.manifest.sources) {
|
|
130
|
+
try {
|
|
131
|
+
const file = fs_1.default.realpathSync.native(path_1.default.resolve(root, source.path));
|
|
132
|
+
if (!within(root, file))
|
|
133
|
+
return unknown(`Plan source ${source.path} escapes its physical project root.`);
|
|
134
|
+
identities.push(JSON.stringify(['source', source.id, contractFileIdentity(file)]));
|
|
135
|
+
for (const registry of physicalRegistries)
|
|
136
|
+
if (within(registry.root, file))
|
|
137
|
+
consumed.add(`registry:${registry.name}`);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return unknown(`Plan source ${source.path} cannot be inspected as a bounded regular file.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const provider = (0, providers_1.providerFor)(target);
|
|
144
|
+
const directories = [provider.skill.global, path_1.default.resolve(root, provider.skill.local)].filter((directory) => directory !== null);
|
|
145
|
+
for (const directory of directories) {
|
|
146
|
+
try {
|
|
147
|
+
if (!directoryExists(directory))
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return unknown(`Provider ${target} install directory ${directory} is unsafe or unreadable.`);
|
|
152
|
+
}
|
|
153
|
+
for (const contract of FRAMEWORK_CONTRACTS) {
|
|
154
|
+
const artifact = path_1.default.join(directory, (0, registry_1.renderedFilename)(contract, provider.skill.renderer));
|
|
155
|
+
try {
|
|
156
|
+
fs_1.default.lstatSync(artifact);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error.code === 'ENOENT')
|
|
160
|
+
continue;
|
|
161
|
+
return unknown(`Runtime artifact ${artifact} cannot be inspected.`);
|
|
162
|
+
}
|
|
163
|
+
const declaredFile = provider.skill.renderer === 'link' ? path_1.default.join(artifact, 'SKILL.md') : artifact;
|
|
164
|
+
try {
|
|
165
|
+
const file = fs_1.default.realpathSync.native(declaredFile);
|
|
166
|
+
const identity = contractFileIdentity(file);
|
|
167
|
+
const owners = physicalRegistries.filter(registry => within(registry.root, file));
|
|
168
|
+
if (owners.length !== 1)
|
|
169
|
+
return unknown(`Runtime artifact ${artifact} has no unique physical registry owner (copied/rendered content is not ownership proof).`);
|
|
170
|
+
consumed.add(`registry:${owners[0].name}`);
|
|
171
|
+
identities.push(JSON.stringify(['artifact', target, contract, provider.skill.renderer, identity]));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return unknown(`Runtime artifact ${artifact} is dangling, nonregular, unreadable or unbounded.`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const registry of physicalRegistries)
|
|
179
|
+
if (consumed.has(`registry:${registry.name}`))
|
|
180
|
+
identities.push(JSON.stringify(['registry', registry.name, registry.identity, registry.remote]));
|
|
181
|
+
return { provenance: 'proven', consumedRegistryComponents: [...consumed].sort(), identityDigest: crypto_1.default.createHash('sha256').update(identities.sort().join('\0')).digest('hex') };
|
|
182
|
+
}
|
|
183
|
+
/** Only the declared CLI floor is consumed here, not a new registry catalog.
|
|
184
|
+
* The native bounded/no-follow read prevents stat/read races and body leaks. */
|
|
185
|
+
function registryCompatibility(registries, consumed) {
|
|
186
|
+
const diagnostics = [];
|
|
187
|
+
const current = (0, cli_version_1.cliVersion)();
|
|
188
|
+
for (const registry of registries) {
|
|
189
|
+
const component = `registry:${registry.name}`;
|
|
190
|
+
if (!consumed.includes(component))
|
|
191
|
+
continue;
|
|
192
|
+
try {
|
|
193
|
+
const physicalRoot = physicalDirectory(registry.contentRoot);
|
|
194
|
+
const manifest = path_1.default.join(physicalRoot, registries_1.REGISTRY_MANIFEST_NAME);
|
|
195
|
+
try {
|
|
196
|
+
fs_1.default.lstatSync(manifest);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error.code === 'ENOENT')
|
|
200
|
+
continue;
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
const bytes = native_bridge_1.secureFs.readRegularFile(manifest, MAX_MANIFEST_BYTES).bytes;
|
|
204
|
+
const raw = (0, json_1.parseJsonNoDuplicate)(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
|
|
205
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
206
|
+
throw new Error('manifest must be a JSON object');
|
|
207
|
+
const min = raw.minCliVersion;
|
|
208
|
+
if (min !== undefined && (typeof min !== 'string' || !/^v?\d+\.\d+\.\d+$/.test(min)
|
|
209
|
+
|| min.replace(/^v/, '').split('.').some(part => !Number.isSafeInteger(Number(part)))))
|
|
210
|
+
throw new Error('invalid CLI floor');
|
|
211
|
+
if (typeof min === 'string' && (0, versioning_1.compareSemver)(current, min) < 0)
|
|
212
|
+
diagnostics.push({ code: 'ADMISSION_REGISTRY_CLI_INCOMPATIBLE', message: `${component} requires CLI >= ${min}; installed CLI is ${current}. Update the CLI before dispatch.` });
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
diagnostics.push({ code: 'ADMISSION_REGISTRY_COMPATIBILITY_UNVERIFIABLE', message: `${component} manifest compatibility could not be verified as bounded regular JSON. Repair the registry before dispatch.` });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return diagnostics;
|
|
219
|
+
}
|
|
220
|
+
/** One admission authority for the public command AND actual watch dispatch.
|
|
221
|
+
* No empirical execution occurs before provenance/currentness/CLI-floor gates. */
|
|
222
|
+
async function admitRegistryPlan(input, dependencies = {}) {
|
|
223
|
+
const admission = dependencies.admitPlan ?? index_1.admitPlan;
|
|
224
|
+
if (input.plan.state !== 'valid' || !(0, providers_1.isAgentTarget)(input.provider) || (input.enabledAgents && !input.enabledAgents.includes(input.provider))
|
|
225
|
+
|| (input.executionMode !== undefined && input.executionMode !== 'interactivo' && input.executionMode !== 'desatendido'))
|
|
226
|
+
return admission(input);
|
|
227
|
+
let registries = [];
|
|
228
|
+
let scope;
|
|
229
|
+
const inventory = dependencies.listRegistries ?? registries_1.listRegistries;
|
|
230
|
+
const target = input.provider;
|
|
231
|
+
try {
|
|
232
|
+
registries = inventory();
|
|
233
|
+
scope = consumedRegistryContracts(input.plan, input.cwd, registries, target);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
scope = unknown('Registry inventory cannot be read.');
|
|
237
|
+
}
|
|
238
|
+
const refresh = () => {
|
|
239
|
+
try {
|
|
240
|
+
const refreshedRegistries = inventory();
|
|
241
|
+
const refreshed = consumedRegistryContracts(input.plan, input.cwd, refreshedRegistries, target);
|
|
242
|
+
const previousComponents = scope.consumedRegistryComponents.join('\0');
|
|
243
|
+
const previousIdentity = scope.identityDigest;
|
|
244
|
+
registries = refreshedRegistries;
|
|
245
|
+
scope = refreshed.provenance === 'unknown' ? refreshed
|
|
246
|
+
: refreshed.consumedRegistryComponents.join('\0') !== previousComponents || refreshed.identityDigest !== previousIdentity
|
|
247
|
+
? unknown('Consumed registry ownership changed during an asynchronous admission observation. Re-run actual currentness and read-only plan admission.') : refreshed;
|
|
248
|
+
if (scope.provenanceDiagnostic)
|
|
249
|
+
scope = { ...scope, provenanceDiagnostic: { ...scope.provenanceDiagnostic, message: `${scope.provenanceDiagnostic.message} Re-run actual currentness after repair.` } };
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
scope = unknown('Registry inventory changed or cannot be read after asynchronous admission. Re-run actual currentness.');
|
|
253
|
+
}
|
|
254
|
+
return scope.provenance === 'proven';
|
|
255
|
+
};
|
|
256
|
+
const compose = async (fields) => {
|
|
257
|
+
const report = await admission({ ...input, provenance: scope.provenance, consumedRegistryComponents: scope.consumedRegistryComponents, ...fields });
|
|
258
|
+
if (scope.provenanceDiagnostic && report.diagnostics.some(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED')) {
|
|
259
|
+
return { ...report, diagnostics: report.diagnostics.map(diagnostic => diagnostic.code === 'ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED' ? scope.provenanceDiagnostic : diagnostic) };
|
|
260
|
+
}
|
|
261
|
+
return report;
|
|
262
|
+
};
|
|
263
|
+
let currentness = input.currentness;
|
|
264
|
+
let compatibilityDiagnostics;
|
|
265
|
+
if (input.requireCurrent) {
|
|
266
|
+
if (scope.provenance !== 'proven')
|
|
267
|
+
return compose({});
|
|
268
|
+
currentness = currentness ?? await (dependencies.checkCurrentness ?? check_1.checkCurrentness)(input.cwd);
|
|
269
|
+
if (!refresh())
|
|
270
|
+
return compose({ currentness });
|
|
271
|
+
// A missing sensor input stages composition before capability resolution.
|
|
272
|
+
const currentnessGate = await compose({ currentness, verifySensors: true, sensors: undefined });
|
|
273
|
+
if (currentnessGate.currentness !== 'current')
|
|
274
|
+
return currentnessGate;
|
|
275
|
+
compatibilityDiagnostics = registryCompatibility(registries, scope.consumedRegistryComponents);
|
|
276
|
+
const compatibilityGate = await compose({ currentness, compatibilityDiagnostics, verifySensors: true, sensors: undefined });
|
|
277
|
+
if (compatibilityGate.currentness !== 'current')
|
|
278
|
+
return compatibilityGate;
|
|
279
|
+
}
|
|
280
|
+
const sensors = input.verifySensors ? input.sensors ?? await (dependencies.runSensors ?? run_1.runSensors)({ cwd: input.cwd, all: true, readOnly: true }) : undefined;
|
|
281
|
+
if (input.requireCurrent && input.verifySensors) {
|
|
282
|
+
if (!refresh())
|
|
283
|
+
return compose({ currentness, sensors });
|
|
284
|
+
const currentnessGate = await compose({ currentness, verifySensors: true, sensors: undefined });
|
|
285
|
+
if (currentnessGate.currentness !== 'current')
|
|
286
|
+
return currentnessGate;
|
|
287
|
+
compatibilityDiagnostics = registryCompatibility(registries, scope.consumedRegistryComponents);
|
|
288
|
+
}
|
|
289
|
+
return compose({ currentness, compatibilityDiagnostics, sensors, ...(dependencies.readRouting ? { routingReader: dependencies.readRouting } : {}) });
|
|
290
|
+
}
|
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.reconcileUnattendedRecovery = reconcileUnattendedRecovery;
|
|
6
7
|
exports.resolveWorkingDirectory = resolveWorkingDirectory;
|
|
7
8
|
exports.computeFingerprint = computeFingerprint;
|
|
8
9
|
const crypto_1 = __importDefault(require("crypto"));
|
|
@@ -19,8 +20,41 @@ function sha(parts) {
|
|
|
19
20
|
// el stderr de git hacia el stderr DEL SUPERVISOR — si ese fd es un pipe roto, el
|
|
20
21
|
// relay dispara un EPIPE no catcheable que crashea el proceso ENTERO (este helper
|
|
21
22
|
// backea computeFingerprint, invocado en CADA tick via FingerprintNow/computeGate).
|
|
23
|
+
/** Git's textual plumbing is hashed, never streamed to a terminal. Keep its
|
|
24
|
+
* memory bounded even for hostile repositories; an incomplete listing must
|
|
25
|
+
* fail recovery rather than certify a partial fingerprint. */
|
|
26
|
+
const MAX_GIT_OUTPUT_BYTES = 16 * 1024 * 1024;
|
|
22
27
|
function git(cwd, args) {
|
|
23
|
-
|
|
28
|
+
try {
|
|
29
|
+
return (0, child_process_1.execFileSync)('git', args, { cwd, encoding: 'utf8', maxBuffer: MAX_GIT_OUTPUT_BYTES, stdio: process_1.EXEC_STDIO });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
const code = error.code;
|
|
33
|
+
if (code === 'ENOBUFS' || error.message.includes('maxBuffer')) {
|
|
34
|
+
throw new Error(`git fingerprint output exceeded ${MAX_GIT_OUTPUT_BYTES} bytes`);
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function reconcileUnattendedRecovery(input) {
|
|
40
|
+
const activeJobIds = [...new Set(input.activeJobIds)].sort();
|
|
41
|
+
const result = (state, nextAction, diagnostics) => ({ state, nextAction, activeJobIds, diagnostics });
|
|
42
|
+
if (input.journalCorrupt || input.journal === null)
|
|
43
|
+
return result('blocked', 'repair-journal', ['journal-corrupt-or-missing']);
|
|
44
|
+
const binding = input.journal.schema === 2 ? input.journal.planBinding : undefined;
|
|
45
|
+
if (!binding || binding.path !== input.plan.path || binding.digest !== input.plan.digest || binding.schema !== input.plan.schema || binding.executionMode !== 'desatendido')
|
|
46
|
+
return result('blocked', 'rebind-plan', ['journal-plan-binding-stale']);
|
|
47
|
+
if (input.git !== 'current')
|
|
48
|
+
return result('blocked', 'reconcile-git', ['git-fingerprint-changed']);
|
|
49
|
+
if (activeJobIds.length > 0)
|
|
50
|
+
return result('ready', 'reconcile-active-jobs', []);
|
|
51
|
+
if (input.verdicts !== 'current')
|
|
52
|
+
return result('blocked', 'repair-verdicts', [`verdicts-${input.verdicts}`]);
|
|
53
|
+
if (input.tests !== 'pass')
|
|
54
|
+
return result('blocked', 'run-tests', [`tests-${input.tests}`]);
|
|
55
|
+
if (input.sensors !== 'pass')
|
|
56
|
+
return result('blocked', 'run-sensors', [`sensors-${input.sensors}`]);
|
|
57
|
+
return result('ready', 'select-work', []);
|
|
24
58
|
}
|
|
25
59
|
function resolveWorkingDirectory(repoRoot, cwdRel) {
|
|
26
60
|
if (typeof cwdRel !== 'string' || cwdRel.length === 0)
|
|
@@ -13,6 +13,7 @@ exports.acksDir = acksDir;
|
|
|
13
13
|
exports.logsDir = logsDir;
|
|
14
14
|
exports.eventsPath = eventsPath;
|
|
15
15
|
exports.exportDir = exportDir;
|
|
16
|
+
exports.bindingPlanPath = bindingPlanPath;
|
|
16
17
|
const fs_1 = __importDefault(require("fs"));
|
|
17
18
|
const path_1 = __importDefault(require("path"));
|
|
18
19
|
function branchSlug(branch) {
|
|
@@ -63,3 +64,13 @@ function eventsPath(repoRoot, branch) {
|
|
|
63
64
|
function exportDir(repoRoot, branch) {
|
|
64
65
|
return path_1.default.join(journalDir(repoRoot, branch), 'export');
|
|
65
66
|
}
|
|
67
|
+
/** A persisted binding path is always a normalized repository-relative POSIX path. */
|
|
68
|
+
function bindingPlanPath(planPath) {
|
|
69
|
+
if (typeof planPath !== 'string' || planPath.length === 0 || planPath.length > 4096 || /[\u0000-\u001F\u007F-\u009F]/.test(planPath))
|
|
70
|
+
throw new Error('plan path inválido para binding');
|
|
71
|
+
const normalized = path_1.default.posix.normalize(planPath.replace(/\\/g, '/'));
|
|
72
|
+
if (path_1.default.posix.isAbsolute(normalized) || path_1.default.win32.isAbsolute(planPath) || normalized === '..' || normalized.startsWith('../') || normalized === '.') {
|
|
73
|
+
throw new Error('plan path fuera del repositorio para binding');
|
|
74
|
+
}
|
|
75
|
+
return normalized;
|
|
76
|
+
}
|
|
@@ -68,7 +68,7 @@ function emitRequest(repoRoot, branch, env) {
|
|
|
68
68
|
const KNOWN_KINDS = [
|
|
69
69
|
'job-request', 'register-entity', 'controller-heartbeat', 'verdict',
|
|
70
70
|
'track-prepare-request', 'track-freeze-request', 'track-join-request',
|
|
71
|
-
'track-teardown-request', 'track-finalize-request',
|
|
71
|
+
'track-teardown-request', 'track-finalize-request', 'routing-reserve', 'routing-observe',
|
|
72
72
|
];
|
|
73
73
|
function isRecord(x) {
|
|
74
74
|
return typeof x === 'object' && x !== null && !Array.isArray(x);
|
|
@@ -3,15 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.assertJournalInitPaths = assertJournalInitPaths;
|
|
6
7
|
exports.initJournal = initJournal;
|
|
8
|
+
exports.initBoundJournal = initBoundJournal;
|
|
7
9
|
exports.readJournal = readJournal;
|
|
8
10
|
exports.writeJournal = writeJournal;
|
|
11
|
+
exports.rebindJournalPlan = rebindJournalPlan;
|
|
9
12
|
exports.appendEvent = appendEvent;
|
|
10
13
|
const fs_1 = __importDefault(require("fs"));
|
|
11
14
|
const crypto_1 = __importDefault(require("crypto"));
|
|
15
|
+
const path_1 = __importDefault(require("path"));
|
|
12
16
|
const atomic_file_1 = require("../atomic-file");
|
|
13
17
|
const types_1 = require("./types");
|
|
14
18
|
const paths_1 = require("./paths");
|
|
19
|
+
const identity_1 = require("../plan/identity");
|
|
20
|
+
const validate_1 = require("../plan/validate");
|
|
15
21
|
/** Schema 1 evoluciono de forma aditiva durante R1/R5. Normalizamos solamente
|
|
16
22
|
* campos que antes no existian; evidencia legacy queda deliberadamente con
|
|
17
23
|
* fingerprint vacio para que el gate la considere stale, nunca certificada.
|
|
@@ -79,22 +85,116 @@ function normalizeSchemaOne(value) {
|
|
|
79
85
|
}
|
|
80
86
|
return parsed;
|
|
81
87
|
}
|
|
82
|
-
function
|
|
83
|
-
for (const d of
|
|
84
|
-
|
|
88
|
+
function initializeDirectories(repoRoot, branch) {
|
|
89
|
+
for (const d of journalDirectories(repoRoot, branch)) {
|
|
90
|
+
assertControlledDirectory(d);
|
|
91
|
+
if (!fs_1.default.existsSync(d))
|
|
92
|
+
fs_1.default.mkdirSync(d, { recursive: false, mode: 0o700 });
|
|
93
|
+
assertControlledDirectory(d);
|
|
85
94
|
fs_1.default.chmodSync(d, 0o700); // mkdirSync mode es umask-dependiente: fijar explicito (R1.2)
|
|
86
95
|
}
|
|
96
|
+
}
|
|
97
|
+
/** Every journal segment is owned state. Validate it centrally before any
|
|
98
|
+
* directory operation or journal file I/O so a hostile `.awm`/`journal`/branch
|
|
99
|
+
* link cannot redirect writes or reads outside the repository. */
|
|
100
|
+
function journalDirectories(repoRoot, branch) {
|
|
101
|
+
const root = path_1.default.join(repoRoot, '.awm');
|
|
102
|
+
const journal = path_1.default.join(root, 'journal');
|
|
103
|
+
return [root, journal, (0, paths_1.journalDir)(repoRoot, branch), (0, paths_1.requestsDir)(repoRoot, branch), (0, paths_1.acksDir)(repoRoot, branch), (0, paths_1.logsDir)(repoRoot, branch), (0, paths_1.exportDir)(repoRoot, branch)];
|
|
104
|
+
}
|
|
105
|
+
function assertControlledDirectory(directory) {
|
|
106
|
+
try {
|
|
107
|
+
const stat = fs_1.default.lstatSync(directory);
|
|
108
|
+
if (stat.isSymbolicLink())
|
|
109
|
+
throw new Error(`journal path rejects symlink: ${directory}`);
|
|
110
|
+
if (!stat.isDirectory())
|
|
111
|
+
throw new Error(`journal path is not a directory: ${directory}`);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error.code === 'ENOENT')
|
|
115
|
+
return;
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function assertJournalTree(repoRoot, branch) {
|
|
120
|
+
for (const directory of journalDirectories(repoRoot, branch).slice(0, 3))
|
|
121
|
+
assertControlledDirectory(directory);
|
|
122
|
+
}
|
|
123
|
+
/** Inspect every existing bootstrap segment before the caller writes anything. */
|
|
124
|
+
function assertJournalInitPaths(repoRoot, branch) {
|
|
125
|
+
for (const directory of journalDirectories(repoRoot, branch))
|
|
126
|
+
assertControlledDirectory(directory);
|
|
127
|
+
assertJournalFileNotSymlink((0, paths_1.statePath)(repoRoot, branch));
|
|
128
|
+
}
|
|
129
|
+
function assertJournalFileNotSymlink(file) {
|
|
130
|
+
try {
|
|
131
|
+
if (fs_1.default.lstatSync(file).isSymbolicLink())
|
|
132
|
+
throw new Error(`journal file rejects symlink: ${file}`);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error.code === 'ENOENT')
|
|
136
|
+
return;
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function initJournal(repoRoot, branch) {
|
|
141
|
+
initializeDirectories(repoRoot, branch);
|
|
87
142
|
const sp = (0, paths_1.statePath)(repoRoot, branch);
|
|
143
|
+
assertJournalFileNotSymlink(sp);
|
|
88
144
|
if (!fs_1.default.existsSync(sp)) {
|
|
89
145
|
(0, atomic_file_1.writeFileAtomicDurable)(sp, JSON.stringify((0, types_1.emptyState)(branch), null, 2) + '\n', 0o600);
|
|
90
146
|
}
|
|
91
147
|
}
|
|
148
|
+
/** Create, never replace, the schema-2 state. The final hard-link is an
|
|
149
|
+
* exclusive atomic publication: another initializer wins cleanly and the
|
|
150
|
+
* completed prior journal is left untouched. */
|
|
151
|
+
function initBoundJournal(repoRoot, branch, binding, report) {
|
|
152
|
+
if (binding.executionDigest !== undefined)
|
|
153
|
+
assertBindingReport(binding, report);
|
|
154
|
+
initializeDirectories(repoRoot, branch);
|
|
155
|
+
const sp = (0, paths_1.statePath)(repoRoot, branch);
|
|
156
|
+
assertJournalFileNotSymlink(sp);
|
|
157
|
+
const initial = { ...(0, types_1.emptyState)(branch), schema: 2, planBinding: binding };
|
|
158
|
+
if (!(0, types_1.isWellFormedState)(initial))
|
|
159
|
+
throw new Error('binding de plan invalido');
|
|
160
|
+
const temporary = path_1.default.join((0, paths_1.journalDir)(repoRoot, branch), `.state.${process.pid}.${crypto_1.default.randomUUID()}.tmp`);
|
|
161
|
+
let fd;
|
|
162
|
+
try {
|
|
163
|
+
fd = fs_1.default.openSync(temporary, 'wx', 0o600);
|
|
164
|
+
fs_1.default.writeFileSync(fd, JSON.stringify(initial, null, 2) + '\n', 'utf8');
|
|
165
|
+
fs_1.default.fsyncSync(fd);
|
|
166
|
+
fs_1.default.closeSync(fd);
|
|
167
|
+
fd = undefined;
|
|
168
|
+
try {
|
|
169
|
+
fs_1.default.linkSync(temporary, sp);
|
|
170
|
+
(0, atomic_file_1.fsyncDirSync)(path_1.default.dirname(sp));
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error.code === 'EEXIST')
|
|
174
|
+
throw new Error('refusing to overwrite existing journal');
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
if (fd !== undefined)
|
|
180
|
+
try {
|
|
181
|
+
fs_1.default.closeSync(fd);
|
|
182
|
+
}
|
|
183
|
+
catch { /* preserve primary error */ }
|
|
184
|
+
try {
|
|
185
|
+
fs_1.default.rmSync(temporary, { force: true });
|
|
186
|
+
}
|
|
187
|
+
catch { /* preserve primary error */ }
|
|
188
|
+
}
|
|
189
|
+
}
|
|
92
190
|
/** Lectura corrupt-aware (R1.6): sintaxis invalida O shape invalido => corrupt:true.
|
|
93
191
|
* Los CONSUMIDORES deciden: consultas muestran 'corrupt'; gate/reconcile bloquean. */
|
|
94
192
|
function readJournal(repoRoot, branch) {
|
|
95
193
|
const sp = (0, paths_1.statePath)(repoRoot, branch);
|
|
96
194
|
let raw;
|
|
97
195
|
try {
|
|
196
|
+
assertJournalTree(repoRoot, branch);
|
|
197
|
+
assertJournalFileNotSymlink(sp);
|
|
98
198
|
raw = fs_1.default.readFileSync(sp, 'utf8');
|
|
99
199
|
}
|
|
100
200
|
catch {
|
|
@@ -125,13 +225,55 @@ function writeJournal(repoRoot, branch, state) {
|
|
|
125
225
|
const next = { ...state, revision: state.revision + 1 };
|
|
126
226
|
if (!(0, types_1.isWellFormedState)(next))
|
|
127
227
|
throw new Error('writeJournal: estado propuesto con forma invalida, no se persiste (R1.6)');
|
|
228
|
+
assertJournalTree(repoRoot, branch);
|
|
229
|
+
assertJournalFileNotSymlink((0, paths_1.statePath)(repoRoot, branch));
|
|
128
230
|
(0, atomic_file_1.writeFileAtomicDurable)((0, paths_1.statePath)(repoRoot, branch), JSON.stringify(next, null, 2) + '\n', 0o600);
|
|
129
231
|
}
|
|
232
|
+
/** Explicit, guarded schema-2 binding transition. It intentionally uses the
|
|
233
|
+
* normal journal write path: state and its audit history are published in one
|
|
234
|
+
* atomic durable replacement, while initBoundJournal remains create-only. */
|
|
235
|
+
function rebindJournalPlan(repoRoot, branch, binding, report) {
|
|
236
|
+
const current = readJournal(repoRoot, branch);
|
|
237
|
+
if (current.corrupt || current.state === null)
|
|
238
|
+
throw new Error('journal inexistente o corrupto: no se puede reconciliar binding');
|
|
239
|
+
if (current.state.schema !== 2 || !current.state.planBinding)
|
|
240
|
+
throw new Error('journal no tiene un binding desatendido reconciliable');
|
|
241
|
+
const previous = current.state.planBinding;
|
|
242
|
+
if (!previous.executionDigest || previous.executionDigest !== binding.executionDigest
|
|
243
|
+
|| previous.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA || binding.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA
|
|
244
|
+
|| previous.path !== binding.path || previous.schema !== binding.schema || previous.executionMode !== binding.executionMode) {
|
|
245
|
+
throw new Error('rebind requires proved unchanged execution identity');
|
|
246
|
+
}
|
|
247
|
+
assertBindingReport(binding, report);
|
|
248
|
+
const next = {
|
|
249
|
+
...current.state,
|
|
250
|
+
planBinding: binding,
|
|
251
|
+
planBindingHistory: [...(current.state.planBindingHistory ?? []), current.state.planBinding],
|
|
252
|
+
};
|
|
253
|
+
writeJournal(repoRoot, branch, next);
|
|
254
|
+
return { ...next, revision: next.revision + 1 };
|
|
255
|
+
}
|
|
256
|
+
/** The old commitment was emitted at initialization/rebind from trusted core
|
|
257
|
+
* validation. Only a new immutable report can demonstrate the same execution
|
|
258
|
+
* identity. Snapshot bytes stay process-private and are never written/exported. */
|
|
259
|
+
function assertBindingReport(binding, report) {
|
|
260
|
+
if (!report || report.state !== 'valid')
|
|
261
|
+
throw new Error('binding execution identity requires a verified plan report');
|
|
262
|
+
const snapshot = (0, validate_1.verifiedPlanSnapshot)(report);
|
|
263
|
+
if (binding.executionIdentitySchema !== identity_1.EXECUTION_IDENTITY_SCHEMA || report.planDigest !== binding.digest
|
|
264
|
+
|| report.executionDigest !== binding.executionDigest || report.schema !== binding.schema
|
|
265
|
+
|| report.executionMode !== binding.executionMode || (0, identity_1.fullPlanDigest)(snapshot) !== binding.digest
|
|
266
|
+
|| (0, identity_1.executionPlanDigest)(snapshot) !== binding.executionDigest)
|
|
267
|
+
throw new Error('verified plan snapshot mismatches binding commitment');
|
|
268
|
+
}
|
|
130
269
|
/** Auditoria derivada best-effort (R4.6): la escribe SOLO el supervisor, un
|
|
131
270
|
* fallo aqui jamas invalida el estado — state.json es la unica autoridad. */
|
|
132
271
|
function appendEvent(repoRoot, branch, event) {
|
|
133
272
|
try {
|
|
134
|
-
|
|
273
|
+
assertJournalTree(repoRoot, branch);
|
|
274
|
+
const ep = (0, paths_1.eventsPath)(repoRoot, branch);
|
|
275
|
+
assertJournalFileNotSymlink(ep);
|
|
276
|
+
fs_1.default.appendFileSync(ep, JSON.stringify({ at: new Date().toISOString(), ...event }) + '\n', { mode: 0o600 });
|
|
135
277
|
}
|
|
136
278
|
catch {
|
|
137
279
|
// best-effort: un evento perdido no se reconstruye ni bloquea (R4.6)
|