agentic-workflow-manager 6.5.2 → 8.0.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/dist/scripts/sensor-support-matrix.js +103 -0
- package/dist/scripts/support-matrix.js +40 -17
- package/dist/src/commands/init.js +1 -3
- package/dist/src/commands/ledger/index.js +6 -0
- package/dist/src/commands/preflight/checks.js +4 -4
- package/dist/src/commands/preflight/index.js +2 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +5 -0
- package/dist/src/commands/registry/status.js +7 -0
- package/dist/src/commands/sensors/compatibility/contract.js +261 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +59 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +197 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +126 -0
- package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
- package/dist/src/commands/sensors/compatibility/probe.js +47 -0
- package/dist/src/commands/sensors/compatibility/resolve.js +96 -0
- package/dist/src/commands/sensors/compatibility/types.js +2 -0
- package/dist/src/commands/sensors/coverage/contract.js +0 -50
- package/dist/src/commands/sensors/coverage/empirical.js +131 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
- package/dist/src/commands/sensors/coverage/evidence.js +5 -1
- package/dist/src/commands/sensors/coverage/index.js +44 -7
- package/dist/src/commands/sensors/coverage/render.js +214 -14
- package/dist/src/commands/sensors/coverage/resolve.js +24 -38
- package/dist/src/commands/sensors/exec.js +150 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +92 -6
- package/dist/src/commands/sensors/run.js +71 -0
- package/dist/src/commands/sensors/status.js +48 -4
- package/dist/src/commands/sync.js +7 -0
- package/dist/src/core/bundles.js +38 -3
- package/dist/src/core/init/steps.js +7 -4
- package/dist/src/core/ledger/scan.js +228 -0
- package/dist/src/core/ledger/store.js +89 -25
- package/dist/src/core/ledger/types.js +54 -0
- package/dist/src/core/paths.js +10 -9
- package/dist/src/core/registries.js +134 -14
- package/dist/src/index.js +24 -1
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/preflight/preflight.test.js +64 -64
- package/dist/tests/commands/registry/add.test.js +69 -2
- package/dist/tests/commands/registry/status.test.js +14 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +98 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +68 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +53 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +27 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +75 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
- package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
- package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
- package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
- package/dist/tests/commands/sensors/exec-windows.test.js +92 -2
- package/dist/tests/commands/sensors/exec.test.js +67 -5
- package/dist/tests/commands/sensors/index.test.js +8 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
- package/dist/tests/commands/sensors/init.test.js +132 -48
- package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +66 -3
- package/dist/tests/commands/sensors/status-windows.test.js +7 -7
- package/dist/tests/commands/sensors/status.test.js +74 -23
- package/dist/tests/commands/update.test.js +1 -1
- package/dist/tests/core/bundles.test.js +37 -0
- package/dist/tests/core/discovery.test.js +4 -0
- package/dist/tests/core/init/orchestrator.test.js +5 -1
- package/dist/tests/core/init/steps.test.js +20 -4
- package/dist/tests/core/ledger/scan.test.js +217 -0
- package/dist/tests/core/ledger/store.test.js +108 -0
- package/dist/tests/core/profile-pins.test.js +2 -1
- package/dist/tests/core/project-skill-links.test.js +22 -0
- package/dist/tests/core/registries-capability.test.js +10 -0
- package/dist/tests/core/registries-sync.test.js +16 -0
- package/dist/tests/core/registries.test.js +129 -1
- package/dist/tests/core/registry-manifest.test.js +7 -0
- package/dist/tests/core/same-existing-path.test.js +35 -0
- package/dist/tests/core/sync-gates.test.js +2 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
- package/dist/tests/structural/active-documentation.test.js +128 -0
- package/dist/tests/structural/async-entrypoint.test.js +15 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
- package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
- package/package.json +8 -2
|
@@ -12,8 +12,75 @@ exports.archiveLedger = archiveLedger;
|
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const child_process_1 = require("child_process");
|
|
15
|
+
const types_1 = require("./types");
|
|
15
16
|
const cluster_1 = require("./cluster");
|
|
16
17
|
const LEDGER_DIR = path_1.default.join('.awm', 'ledger');
|
|
18
|
+
function isWithin(root, candidate) {
|
|
19
|
+
const relative = path_1.default.relative(root, candidate);
|
|
20
|
+
return relative === '' || (!relative.startsWith(`..${path_1.default.sep}`) && relative !== '..' && !path_1.default.isAbsolute(relative));
|
|
21
|
+
}
|
|
22
|
+
function assertSafeDirectory(directory, root, name) {
|
|
23
|
+
const stat = fs_1.default.lstatSync(directory);
|
|
24
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
25
|
+
throw new Error(`unsafe ledger ${name} directory`);
|
|
26
|
+
const real = fs_1.default.realpathSync(directory);
|
|
27
|
+
if (!isWithin(root, real))
|
|
28
|
+
throw new Error(`ledger ${name} directory escapes its root`);
|
|
29
|
+
return real;
|
|
30
|
+
}
|
|
31
|
+
function sourceExists(source) {
|
|
32
|
+
try {
|
|
33
|
+
fs_1.default.lstatSync(source);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
if (error.code === 'ENOENT')
|
|
38
|
+
return false;
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function assertSafeLedgerSource(source, ledgerRoot) {
|
|
43
|
+
const stat = fs_1.default.lstatSync(source);
|
|
44
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
45
|
+
throw new Error('unsafe ledger source');
|
|
46
|
+
const real = fs_1.default.realpathSync(source);
|
|
47
|
+
if (!isWithin(ledgerRoot, real))
|
|
48
|
+
throw new Error('ledger source escapes ledger directory');
|
|
49
|
+
}
|
|
50
|
+
function ledgerFilename(branch) {
|
|
51
|
+
if (typeof branch !== 'string'
|
|
52
|
+
|| branch.length === 0
|
|
53
|
+
|| branch.includes('\\')
|
|
54
|
+
|| path_1.default.posix.isAbsolute(branch)
|
|
55
|
+
|| path_1.default.win32.isAbsolute(branch)
|
|
56
|
+
|| branch.split('/').some(segment => segment === '.' || segment === '..')) {
|
|
57
|
+
throw new Error('invalid ledger branch');
|
|
58
|
+
}
|
|
59
|
+
return branch.replace(/\//g, '__');
|
|
60
|
+
}
|
|
61
|
+
function archiveLabel(label) {
|
|
62
|
+
if (typeof label !== 'string'
|
|
63
|
+
|| label.length === 0
|
|
64
|
+
|| label === '.'
|
|
65
|
+
|| label.includes('..')
|
|
66
|
+
|| /[/\\]/.test(label)
|
|
67
|
+
|| path_1.default.posix.isAbsolute(label)
|
|
68
|
+
|| path_1.default.win32.isAbsolute(label)) {
|
|
69
|
+
throw new Error('invalid ledger label');
|
|
70
|
+
}
|
|
71
|
+
return label;
|
|
72
|
+
}
|
|
73
|
+
function destinationExists(destination) {
|
|
74
|
+
try {
|
|
75
|
+
fs_1.default.lstatSync(destination);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error.code === 'ENOENT')
|
|
80
|
+
return false;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
17
84
|
function detectBranch(cwd) {
|
|
18
85
|
try {
|
|
19
86
|
const b = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
|
|
@@ -26,28 +93,15 @@ function detectBranch(cwd) {
|
|
|
26
93
|
}
|
|
27
94
|
}
|
|
28
95
|
function ledgerPath(cwd, branch) {
|
|
29
|
-
|
|
30
|
-
return path_1.default.join(cwd, LEDGER_DIR, `${safe}.jsonl`);
|
|
96
|
+
return path_1.default.join(cwd, LEDGER_DIR, `${ledgerFilename(branch)}.jsonl`);
|
|
31
97
|
}
|
|
32
98
|
function addEntry(cwd, entry) {
|
|
99
|
+
const parsed = (0, types_1.parseLedgerEntry)(entry, 'ledger entry');
|
|
100
|
+
if (!parsed.ok)
|
|
101
|
+
throw new Error(`invalid ledger entry: ${parsed.reason}`);
|
|
33
102
|
const p = ledgerPath(cwd, entry.branch);
|
|
34
103
|
fs_1.default.mkdirSync(path_1.default.dirname(p), { recursive: true });
|
|
35
|
-
fs_1.default.appendFileSync(p, JSON.stringify(entry) + '\n', 'utf-8');
|
|
36
|
-
}
|
|
37
|
-
/** Required LedgerEntry fields that `cluster.ts` reads unconditionally
|
|
38
|
-
* (`signature`, `desc`, `ref` when present). A JSONL line can be syntactically
|
|
39
|
-
* valid JSON while still being shape-invalid (e.g. missing `desc`) — that's
|
|
40
|
-
* not a parse error, so it needs its own check, extending the same "skip
|
|
41
|
-
* malformed line" policy this function already applies to JSON syntax errors. */
|
|
42
|
-
function isWellFormedEntry(x) {
|
|
43
|
-
if (!x || typeof x !== 'object')
|
|
44
|
-
return false;
|
|
45
|
-
const e = x;
|
|
46
|
-
return typeof e.signature === 'string'
|
|
47
|
-
&& typeof e.desc === 'string'
|
|
48
|
-
&& typeof e.branch === 'string'
|
|
49
|
-
&& typeof e.polarity === 'string'
|
|
50
|
-
&& (e.ref === undefined || typeof e.ref === 'string');
|
|
104
|
+
fs_1.default.appendFileSync(p, JSON.stringify(parsed.entry) + '\n', 'utf-8');
|
|
51
105
|
}
|
|
52
106
|
function listEntries(cwd, branch) {
|
|
53
107
|
const p = ledgerPath(cwd, branch);
|
|
@@ -59,9 +113,9 @@ function listEntries(cwd, branch) {
|
|
|
59
113
|
if (!trimmed)
|
|
60
114
|
continue;
|
|
61
115
|
try {
|
|
62
|
-
const parsed = JSON.parse(trimmed);
|
|
63
|
-
if (
|
|
64
|
-
out.push(parsed);
|
|
116
|
+
const parsed = (0, types_1.parseLedgerEntry)(JSON.parse(trimmed), p);
|
|
117
|
+
if (parsed.ok)
|
|
118
|
+
out.push(parsed.entry);
|
|
65
119
|
}
|
|
66
120
|
catch { /* skip malformed line */ }
|
|
67
121
|
}
|
|
@@ -71,12 +125,22 @@ function recurring(cwd, branch, min) {
|
|
|
71
125
|
return (0, cluster_1.clusterEntries)(listEntries(cwd, branch), min);
|
|
72
126
|
}
|
|
73
127
|
function archiveLedger(cwd, branch, label) {
|
|
128
|
+
const safeLabel = archiveLabel(label);
|
|
74
129
|
const src = ledgerPath(cwd, branch);
|
|
75
|
-
if (!
|
|
130
|
+
if (!sourceExists(src))
|
|
76
131
|
return false;
|
|
77
|
-
const safe = branch
|
|
78
|
-
const
|
|
79
|
-
|
|
132
|
+
const safe = ledgerFilename(branch);
|
|
133
|
+
const projectRoot = fs_1.default.realpathSync(cwd);
|
|
134
|
+
const ledgerRoot = path_1.default.join(projectRoot, LEDGER_DIR);
|
|
135
|
+
const safeLedgerRoot = assertSafeDirectory(ledgerRoot, projectRoot, 'root');
|
|
136
|
+
assertSafeLedgerSource(src, safeLedgerRoot);
|
|
137
|
+
const archiveRoot = path_1.default.join(ledgerRoot, 'archive');
|
|
138
|
+
if (!sourceExists(archiveRoot))
|
|
139
|
+
fs_1.default.mkdirSync(archiveRoot);
|
|
140
|
+
const safeArchiveRoot = assertSafeDirectory(archiveRoot, safeLedgerRoot, 'archive');
|
|
141
|
+
const dst = path_1.default.join(safeArchiveRoot, `${safe}-${safeLabel}.jsonl`);
|
|
142
|
+
if (destinationExists(dst))
|
|
143
|
+
throw new Error(`ledger archive already exists: ${dst}`);
|
|
80
144
|
fs_1.default.renameSync(src, dst);
|
|
81
145
|
return true;
|
|
82
146
|
}
|
|
@@ -1,2 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFECT_CLASS = void 0;
|
|
4
|
+
exports.parseLedgerEntry = parseLedgerEntry;
|
|
5
|
+
exports.DEFECT_CLASS = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function isPolarity(value) {
|
|
10
|
+
return value === 'win' || value === 'finding';
|
|
11
|
+
}
|
|
12
|
+
function isLedgerClass(value) {
|
|
13
|
+
return value === 'structural' || value === 'logica' || value === 'proceso' || value === 'seguridad';
|
|
14
|
+
}
|
|
15
|
+
function isSeverity(value) {
|
|
16
|
+
return value === 'blocker' || value === 'important' || value === 'minor' || value === 'info';
|
|
17
|
+
}
|
|
18
|
+
/** Validates the durable JSONL boundary. Missing defectClass is deliberately legacy-valid. */
|
|
19
|
+
function parseLedgerEntry(input, source) {
|
|
20
|
+
if (typeof source !== 'string')
|
|
21
|
+
throw new Error('ledger parse source must be a string');
|
|
22
|
+
if (!isRecord(input))
|
|
23
|
+
return { ok: false, source, reason: 'not-object' };
|
|
24
|
+
if (!isPolarity(input.polarity) || !isLedgerClass(input.class) || !isSeverity(input.severity)) {
|
|
25
|
+
return { ok: false, source, reason: 'invalid-enum' };
|
|
26
|
+
}
|
|
27
|
+
for (const field of ['ts', 'branch', 'phase', 'source_skill', 'signature', 'desc']) {
|
|
28
|
+
if (typeof input[field] !== 'string')
|
|
29
|
+
return { ok: false, source, reason: 'invalid-fields' };
|
|
30
|
+
}
|
|
31
|
+
if ('ref' in input && input.ref !== undefined && typeof input.ref !== 'string') {
|
|
32
|
+
return { ok: false, source, reason: 'invalid-ref' };
|
|
33
|
+
}
|
|
34
|
+
if ('defectClass' in input && input.defectClass !== undefined
|
|
35
|
+
&& (typeof input.defectClass !== 'string' || !exports.DEFECT_CLASS.test(input.defectClass))) {
|
|
36
|
+
return { ok: false, source, reason: 'invalid-defect-class' };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
ok: true,
|
|
40
|
+
entry: {
|
|
41
|
+
ts: input.ts,
|
|
42
|
+
branch: input.branch,
|
|
43
|
+
phase: input.phase,
|
|
44
|
+
source_skill: input.source_skill,
|
|
45
|
+
polarity: input.polarity,
|
|
46
|
+
class: input.class,
|
|
47
|
+
signature: input.signature,
|
|
48
|
+
severity: input.severity,
|
|
49
|
+
desc: input.desc,
|
|
50
|
+
...(typeof input.ref === 'string' ? { ref: input.ref } : {}),
|
|
51
|
+
// Keep the durable legacy distinction visible to consumers: an older
|
|
52
|
+
// entry is valid, but has no reusable classification yet.
|
|
53
|
+
defectClass: typeof input.defectClass === 'string' ? input.defectClass : undefined,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/src/core/paths.js
CHANGED
|
@@ -174,10 +174,11 @@ function resolveOnPath(bin) {
|
|
|
174
174
|
* cohorte no llegaba nunca ni a `SERIAL` ni a `ACTIVE`. Un solo bug de comparación, quince
|
|
175
175
|
* tests en cascada.
|
|
176
176
|
*
|
|
177
|
-
* La identidad se toma
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
177
|
+
* La identidad se toma primero de `realpathSync.native`, que resuelve la ruta con la API
|
|
178
|
+
* del SO (incluyendo nombres cortos 8.3) antes de normalizar separadores y capitalización.
|
|
179
|
+
* En POSIX, dos hard links de archivo conservan el mismo `(dev, ino)` aunque el realpath
|
|
180
|
+
* mantenga sus nombres distintos; ese fallback preserva su identidad. En Windows no se usa:
|
|
181
|
+
* dos directorios temporales distintos pueden reportar el mismo par de metadatos.
|
|
181
182
|
*
|
|
182
183
|
* Cualquier fallo de `stat` significa que al menos una no existe: `false`. La identidad
|
|
183
184
|
* nunca se afirma sin prueba.
|
|
@@ -192,10 +193,6 @@ function sameExistingPath(a, b) {
|
|
|
192
193
|
catch {
|
|
193
194
|
return false;
|
|
194
195
|
}
|
|
195
|
-
// `ino === 0` en AMBOS lados no prueba identidad: probaría que el filesystem no la
|
|
196
|
-
// reporta. Tratarlo como match haría coincidir dos rutas cualesquiera del mismo volumen.
|
|
197
|
-
if (sa.ino !== 0 && sb.ino !== 0)
|
|
198
|
-
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
199
196
|
const canonical = (p) => {
|
|
200
197
|
try {
|
|
201
198
|
const real = fs_1.default.realpathSync.native(p).replaceAll('\\', '/');
|
|
@@ -207,5 +204,9 @@ function sameExistingPath(a, b) {
|
|
|
207
204
|
};
|
|
208
205
|
const ca = canonical(a);
|
|
209
206
|
const cb = canonical(b);
|
|
210
|
-
|
|
207
|
+
if (ca === null || cb === null)
|
|
208
|
+
return false;
|
|
209
|
+
if (ca === cb)
|
|
210
|
+
return true;
|
|
211
|
+
return !isWindowsNative() && sa.ino !== 0 && sb.ino !== 0 && sa.dev === sb.dev && sa.ino === sb.ino;
|
|
211
212
|
}
|
|
@@ -3,7 +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.REGISTRY_MANIFEST_NAME = exports.CONTENT_DIR_NAMES = void 0;
|
|
6
|
+
exports.REGISTRY_METADATA_FILE_NAMES = exports.REGISTRY_MANIFEST_NAME = exports.REGISTRY_DIR_NAMES = exports.CAPABILITY_DIR_NAMES = exports.CONTENT_DIR_NAMES = void 0;
|
|
7
7
|
exports.registriesDir = registriesDir;
|
|
8
8
|
exports.registriesConfigPath = registriesConfigPath;
|
|
9
9
|
exports.registryContentRoot = registryContentRoot;
|
|
@@ -13,7 +13,10 @@ exports.seedBaselineRegistry = seedBaselineRegistry;
|
|
|
13
13
|
exports.listRegistries = listRegistries;
|
|
14
14
|
exports.contentRoots = contentRoots;
|
|
15
15
|
exports.capabilityRoot = capabilityRoot;
|
|
16
|
+
exports.assertRegularRegistryFile = assertRegularRegistryFile;
|
|
17
|
+
exports.isSafeRegistryRoot = isSafeRegistryRoot;
|
|
16
18
|
exports.validateRegistryLayout = validateRegistryLayout;
|
|
19
|
+
exports.registriesNeedSync = registriesNeedSync;
|
|
17
20
|
exports.syncRegistries = syncRegistries;
|
|
18
21
|
exports.registrySyncErrors = registrySyncErrors;
|
|
19
22
|
exports.describeRegistrySyncErrors = describeRegistrySyncErrors;
|
|
@@ -44,9 +47,13 @@ function registriesConfigPath() {
|
|
|
44
47
|
return path_1.default.join((0, paths_1.awmHome)(), 'registries.json');
|
|
45
48
|
}
|
|
46
49
|
exports.CONTENT_DIR_NAMES = ['skills', 'bundles', 'workflows', 'agents'];
|
|
50
|
+
exports.CAPABILITY_DIR_NAMES = ['hooks', 'sensor-packs'];
|
|
51
|
+
exports.REGISTRY_DIR_NAMES = [...exports.CONTENT_DIR_NAMES, ...exports.CAPABILITY_DIR_NAMES];
|
|
52
|
+
exports.REGISTRY_MANIFEST_NAME = 'awm-registry.json';
|
|
53
|
+
exports.REGISTRY_METADATA_FILE_NAMES = [exports.REGISTRY_MANIFEST_NAME, 'catalog.json'];
|
|
47
54
|
function registryContentRoot(name) {
|
|
48
55
|
const root = path_1.default.join(registriesDir(), name);
|
|
49
|
-
if (!path_1.default.resolve(root).startsWith(path_1.default.resolve(registriesDir()) + path_1.default.sep)) {
|
|
56
|
+
if (!name || name === '.' || name.includes('..') || /[/\\]/.test(name) || !path_1.default.resolve(root).startsWith(path_1.default.resolve(registriesDir()) + path_1.default.sep)) {
|
|
50
57
|
throw new Error(`Invalid registry name "${name}" — must not contain path separators`);
|
|
51
58
|
}
|
|
52
59
|
return root;
|
|
@@ -68,7 +75,7 @@ function readRegistriesConfig() {
|
|
|
68
75
|
if (typeof entry?.name !== 'string' || typeof entry?.remote !== 'string') {
|
|
69
76
|
throw new Error(`Invalid registries config at ${registriesConfigPath()}: malformed entry ${JSON.stringify(entry)}`);
|
|
70
77
|
}
|
|
71
|
-
if (entry.name === '.' || entry.name.includes('/') || entry.name.includes('\\') || entry.name.includes('..')) {
|
|
78
|
+
if (!entry.name || entry.name === '.' || entry.name.includes('/') || entry.name.includes('\\') || entry.name.includes('..')) {
|
|
72
79
|
throw new Error(`Invalid registries config at ${registriesConfigPath()}: malformed entry name "${entry.name}" (path traversal)`);
|
|
73
80
|
}
|
|
74
81
|
}
|
|
@@ -95,7 +102,7 @@ function listRegistries() {
|
|
|
95
102
|
function contentRoots() {
|
|
96
103
|
const roots = [];
|
|
97
104
|
for (const reg of listRegistries()) {
|
|
98
|
-
if (
|
|
105
|
+
if (isSafeRegistryRoot(reg.contentRoot))
|
|
99
106
|
roots.push(reg.contentRoot);
|
|
100
107
|
}
|
|
101
108
|
return roots;
|
|
@@ -104,14 +111,121 @@ function contentRoots() {
|
|
|
104
111
|
* directorio pedido ('hooks', 'sensor-packs', 'skills'…). null si ninguno. */
|
|
105
112
|
function capabilityRoot(dirName) {
|
|
106
113
|
for (const root of contentRoots()) {
|
|
107
|
-
if (
|
|
114
|
+
if (isRegularDirectory(path_1.default.join(root, dirName)))
|
|
108
115
|
return root;
|
|
109
116
|
}
|
|
110
117
|
return null;
|
|
111
118
|
}
|
|
112
|
-
/**
|
|
119
|
+
/** Todo symlink bajo directorios consumidos de un registry es no confiable: discovery,
|
|
120
|
+
* hooks y sensores no pueden consumir contenido que podría resolver fuera del clone. */
|
|
121
|
+
function containsManagedSymlink(dir) {
|
|
122
|
+
let entries;
|
|
123
|
+
try {
|
|
124
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
for (const entry of entries) {
|
|
130
|
+
const candidate = path_1.default.join(dir, entry.name);
|
|
131
|
+
let stat;
|
|
132
|
+
try {
|
|
133
|
+
stat = fs_1.default.lstatSync(candidate);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (stat.isSymbolicLink())
|
|
139
|
+
return true;
|
|
140
|
+
if (stat.isDirectory() && containsManagedSymlink(candidate))
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
function isMissing(error) {
|
|
146
|
+
return error?.code === 'ENOENT';
|
|
147
|
+
}
|
|
148
|
+
function isRegularDirectory(candidate) {
|
|
149
|
+
try {
|
|
150
|
+
const stat = fs_1.default.lstatSync(candidate);
|
|
151
|
+
return stat.isDirectory() && !stat.isSymbolicLink();
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function isSafeOptionalRegistryFile(candidate) {
|
|
158
|
+
try {
|
|
159
|
+
const stat = fs_1.default.lstatSync(candidate);
|
|
160
|
+
return stat.isFile() && !stat.isSymbolicLink();
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
return isMissing(error);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/** Returns false when absent; throws when an existing registry metadata file cannot be
|
|
167
|
+
* safely read as a regular file. Kept public so catalog and manifest readers share the
|
|
168
|
+
* same trust boundary instead of relying only on an earlier layout scan. */
|
|
169
|
+
function assertRegularRegistryFile(file) {
|
|
170
|
+
let stat;
|
|
171
|
+
try {
|
|
172
|
+
stat = fs_1.default.lstatSync(file);
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (isMissing(error))
|
|
176
|
+
return false;
|
|
177
|
+
throw new Error(`Cannot inspect registry metadata at ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
178
|
+
}
|
|
179
|
+
if (stat.isSymbolicLink())
|
|
180
|
+
throw new Error(`Registry metadata at ${file} must not be a symbolic link`);
|
|
181
|
+
if (!stat.isFile())
|
|
182
|
+
throw new Error(`Registry metadata at ${file} must be a regular file`);
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
/** Verifica que la raíz y cada directorio que AWM consume sean regulares y no tengan
|
|
186
|
+
* symlinks anidados. Los registries capability-only siguen siendo válidos. */
|
|
187
|
+
function inspectRegistrySafety(root) {
|
|
188
|
+
if (!isRegularDirectory(root))
|
|
189
|
+
return { safe: false, hasManagedDirectory: false };
|
|
190
|
+
let hasManagedDirectory = false;
|
|
191
|
+
for (const d of exports.REGISTRY_DIR_NAMES) {
|
|
192
|
+
const candidate = path_1.default.join(root, d);
|
|
193
|
+
try {
|
|
194
|
+
const stat = fs_1.default.lstatSync(candidate);
|
|
195
|
+
if (stat.isSymbolicLink())
|
|
196
|
+
return { safe: false, hasManagedDirectory: false };
|
|
197
|
+
if (!stat.isDirectory())
|
|
198
|
+
continue;
|
|
199
|
+
hasManagedDirectory = true;
|
|
200
|
+
if (containsManagedSymlink(candidate))
|
|
201
|
+
return { safe: false, hasManagedDirectory: false };
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (isMissing(error))
|
|
205
|
+
continue;
|
|
206
|
+
return { safe: false, hasManagedDirectory: false };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const file of exports.REGISTRY_METADATA_FILE_NAMES) {
|
|
210
|
+
if (!isSafeOptionalRegistryFile(path_1.default.join(root, file))) {
|
|
211
|
+
return { safe: false, hasManagedDirectory: false };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return { safe: true, hasManagedDirectory };
|
|
215
|
+
}
|
|
216
|
+
function isSafeRegistryRoot(root) {
|
|
217
|
+
return inspectRegistrySafety(root).safe;
|
|
218
|
+
}
|
|
219
|
+
/** Un registry válido tiene ≥1 directorio de contenido o capability regular y ningún
|
|
220
|
+
* symlink en las rutas que AWM consume. */
|
|
113
221
|
function validateRegistryLayout(root) {
|
|
114
|
-
|
|
222
|
+
const state = inspectRegistrySafety(root);
|
|
223
|
+
return state.safe && state.hasManagedDirectory;
|
|
224
|
+
}
|
|
225
|
+
/** Whether machine setup must synchronize before any registry-backed discovery.
|
|
226
|
+
* An empty root is safe to keep configured, but cannot yet supply AWM content. */
|
|
227
|
+
function registriesNeedSync() {
|
|
228
|
+
return listRegistries().some((registry) => !validateRegistryLayout(registry.contentRoot));
|
|
115
229
|
}
|
|
116
230
|
/** Sincroniza cada registry configurado al ref resuelto (pin > último tag > HEAD);
|
|
117
231
|
* re-clona si falta el dir. Errores por-registry NO fatales: se reportan en el resultado. */
|
|
@@ -120,6 +234,9 @@ async function syncRegistries() {
|
|
|
120
234
|
for (const reg of listRegistries()) {
|
|
121
235
|
try {
|
|
122
236
|
const freshClone = !fs_1.default.existsSync(reg.contentRoot);
|
|
237
|
+
if (!freshClone && !isSafeRegistryRoot(reg.contentRoot)) {
|
|
238
|
+
throw new Error(`Unsafe registry layout at ${reg.contentRoot}`);
|
|
239
|
+
}
|
|
123
240
|
if (freshClone) {
|
|
124
241
|
fs_1.default.mkdirSync(registriesDir(), { recursive: true });
|
|
125
242
|
try {
|
|
@@ -140,6 +257,9 @@ async function syncRegistries() {
|
|
|
140
257
|
await git.checkout(resolved.ref);
|
|
141
258
|
if (resolved.kind !== 'tag')
|
|
142
259
|
await git.pull('origin', resolved.ref);
|
|
260
|
+
if (!validateRegistryLayout(reg.contentRoot)) {
|
|
261
|
+
throw new Error(`Unsafe registry layout at ${reg.contentRoot}`);
|
|
262
|
+
}
|
|
143
263
|
}
|
|
144
264
|
catch (e) {
|
|
145
265
|
if (freshClone)
|
|
@@ -169,13 +289,14 @@ function describeRegistrySyncErrors(errors) {
|
|
|
169
289
|
return errors.map((e) => `${e.name}: ${e.error}`).join('; ');
|
|
170
290
|
}
|
|
171
291
|
/**
|
|
172
|
-
* Registries that both errored during sync AND have
|
|
173
|
-
* afterwards — i.e. genuinely unusable, as opposed to merely stale.
|
|
292
|
+
* Registries that both errored during sync AND no longer have a usable content
|
|
293
|
+
* layout afterwards — i.e. genuinely unusable, as opposed to merely stale.
|
|
174
294
|
*
|
|
175
295
|
* `syncRegistries()` deliberately reports per-registry failures as results
|
|
176
296
|
* rather than throwing, so a flaky secondary registry never aborts a whole
|
|
177
297
|
* run. Callers that go on to READ registry content (init) still need to know
|
|
178
|
-
* whether what they are about to read
|
|
298
|
+
* whether what they are about to read remains usable: otherwise a missing,
|
|
299
|
+
* empty, or unsafe registry
|
|
179
300
|
* resurfaces much later as an unrelated step's failure, with its real cause
|
|
180
301
|
* already discarded. Note that "usable" is deliberately about content on disk,
|
|
181
302
|
* not about being a healthy git clone — a seeded content root with no `.git`
|
|
@@ -188,7 +309,7 @@ function unusableSyncedRegistries(results) {
|
|
|
188
309
|
const roots = new Map(listRegistries().map((r) => [r.name, r.contentRoot]));
|
|
189
310
|
return errors.filter((e) => {
|
|
190
311
|
const root = roots.get(e.name);
|
|
191
|
-
return root === undefined || !
|
|
312
|
+
return root === undefined || !validateRegistryLayout(root);
|
|
192
313
|
});
|
|
193
314
|
}
|
|
194
315
|
/** `unusableSyncedRegistries` as a guard: throws naming every unusable registry. */
|
|
@@ -196,12 +317,11 @@ function assertSyncedRegistriesUsable(results) {
|
|
|
196
317
|
const unusable = unusableSyncedRegistries(results);
|
|
197
318
|
if (unusable.length === 0)
|
|
198
319
|
return;
|
|
199
|
-
throw new Error(`registry sync failed and left no content on disk — ${describeRegistrySyncErrors(unusable)}`);
|
|
320
|
+
throw new Error(`registry sync failed and left no usable content on disk — ${describeRegistrySyncErrors(unusable)}`);
|
|
200
321
|
}
|
|
201
|
-
exports.REGISTRY_MANIFEST_NAME = 'awm-registry.json';
|
|
202
322
|
function readRegistryManifest(root) {
|
|
203
323
|
const file = path_1.default.join(root, exports.REGISTRY_MANIFEST_NAME);
|
|
204
|
-
if (!
|
|
324
|
+
if (!assertRegularRegistryFile(file))
|
|
205
325
|
return { overrides: new Set() };
|
|
206
326
|
let raw;
|
|
207
327
|
try {
|
package/dist/src/index.js
CHANGED
|
@@ -118,6 +118,15 @@ program.command('add [name]')
|
|
|
118
118
|
if (r.action === 'error')
|
|
119
119
|
console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
|
|
120
120
|
}
|
|
121
|
+
try {
|
|
122
|
+
(0, registries_1.assertSyncedRegistriesUsable)(results);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
s.stop('Registry sync failed.');
|
|
126
|
+
console.error(picocolors_1.default.red(e.message));
|
|
127
|
+
process.exitCode = 1;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
121
130
|
// 1b. If `name` matches a bundle, run the bundle-activation flow and exit.
|
|
122
131
|
if (name) {
|
|
123
132
|
const allBundles = (0, bundles_1.discoverAllBundles)();
|
|
@@ -437,6 +446,14 @@ program.command('list [package]')
|
|
|
437
446
|
if (r.action === 'error')
|
|
438
447
|
console.warn(picocolors_1.default.yellow(` ⚠ registry ${r.name}: ${r.error}`));
|
|
439
448
|
}
|
|
449
|
+
try {
|
|
450
|
+
(0, registries_1.assertSyncedRegistriesUsable)(listSyncResults);
|
|
451
|
+
}
|
|
452
|
+
catch (e) {
|
|
453
|
+
console.error(picocolors_1.default.red(e.message));
|
|
454
|
+
process.exitCode = 1;
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
440
457
|
const fullView = (0, registry_view_1.buildPackageView)((0, discovery_1.discoverSkills)(), (0, discovery_1.discoverWorkflows)(), (0, discovery_1.discoverAgents)(), (0, bundles_1.discoverAllBundles)());
|
|
441
458
|
const view = options.all ? fullView : fullView.filter((p) => p.visibility !== 'private');
|
|
442
459
|
if (view.length === 0) {
|
|
@@ -716,4 +733,10 @@ miroCmd.command('sync <storyMapPath>')
|
|
|
716
733
|
(0, job_1.registerJobCommand)(program);
|
|
717
734
|
(0, watch_1.registerWatchCommand)(program);
|
|
718
735
|
(0, track_1.registerTrackCommand)(program);
|
|
719
|
-
|
|
736
|
+
// Commander only waits for async action handlers through parseAsync(). The CLI has
|
|
737
|
+
// async commands (including `sensors coverage`), so returning its promise keeps the
|
|
738
|
+
// process alive until their JSON/output contract has been completed.
|
|
739
|
+
program.parseAsync().catch((error) => {
|
|
740
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
741
|
+
process.exitCode = 1;
|
|
742
|
+
});
|
|
@@ -40,6 +40,17 @@ describe('awm ledger CLI', () => {
|
|
|
40
40
|
expect(entries[0]).toMatchObject({ signature: 'sig-1', polarity: 'finding', class: 'logica' });
|
|
41
41
|
expect(entries[0].ts).toMatch(/\d{4}-\d{2}-\d{2}T/);
|
|
42
42
|
});
|
|
43
|
+
test('add persists an optional reusable defect class', () => {
|
|
44
|
+
run(['add', '--branch', 'feat-x', '--polarity', 'finding', '--class', 'logica',
|
|
45
|
+
'--signature', 'sig-1', '--severity', 'blocker', '--desc', 'boom', '--defect-class', 'lint-errors'], cwd);
|
|
46
|
+
expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([expect.objectContaining({ defectClass: 'lint-errors' })]);
|
|
47
|
+
});
|
|
48
|
+
test.each(['', 'Bad_ID', '../escape', 'a b', '-leading', 'trailing-'])('add rejects invalid defect class %p before writing', (defectClass) => {
|
|
49
|
+
expect(() => run(['add', '--branch', 'feat-x', '--polarity', 'finding', '--class', 'logica',
|
|
50
|
+
'--signature', 'sig-1', '--severity', 'blocker', '--desc', 'boom', '--defect-class', defectClass], cwd))
|
|
51
|
+
.toThrow(/defect-class.*kebab-case/i);
|
|
52
|
+
expect((0, store_1.listEntries)(cwd, 'feat-x')).toEqual([]);
|
|
53
|
+
});
|
|
43
54
|
test('list emits the branch entries as JSON', () => {
|
|
44
55
|
run(['add', '--branch', 'feat-x', '--polarity', 'win', '--class', 'proceso',
|
|
45
56
|
'--signature', 'good', '--severity', 'info', '--desc', 'nice'], cwd);
|