agentic-workflow-manager 9.7.1 → 9.8.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/plan/index.js +124 -6
- 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/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 +181 -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/store.js +146 -4
- package/dist/src/core/journal/types.js +35 -1
- package/dist/src/core/migration/historical-provenance.js +18 -0
- package/dist/src/core/migration/index.js +322 -0
- package/dist/src/core/plan/identity.js +75 -0
- package/dist/src/core/plan/validate.js +178 -64
- package/dist/tests/commands/job/exec-wrapper.test.js +16 -0
- package/dist/tests/commands/job/gate-reconcile.test.js +24 -10
- package/dist/tests/commands/pin.test.js +23 -23
- package/dist/tests/commands/plan/index.test.js +630 -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/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 +87 -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/plan/validate.test.js +320 -6
- 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/jest-environment-is-isolated.test.js +29 -24
- package/dist/tests/structural/native-release-artifacts.test.js +16 -0
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-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,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROVIDER_EXECUTION_CAPABILITIES = void 0;
|
|
4
|
+
exports.sanitizeAdmissionReport = sanitizeAdmissionReport;
|
|
5
|
+
exports.admitPlan = admitPlan;
|
|
6
|
+
const providers_1 = require("../../providers");
|
|
7
|
+
const paths_1 = require("../journal/paths");
|
|
8
|
+
const UNKNOWN = {
|
|
9
|
+
artifactDelivery: 'unverified', interactiveExecution: 'unverified', unattendedController: 'unverified', nativeSubagents: 'unverified',
|
|
10
|
+
modelOverride: 'unverified', effortOverride: 'unverified', observedModelEvidence: 'unverified', durableResume: 'unverified',
|
|
11
|
+
};
|
|
12
|
+
/** Deliberate R1 evidence table. Artifact delivery is never used as execution evidence. */
|
|
13
|
+
exports.PROVIDER_EXECUTION_CAPABILITIES = {
|
|
14
|
+
antigravity: { ...UNKNOWN },
|
|
15
|
+
opencode: { ...UNKNOWN },
|
|
16
|
+
'claude-code': { ...UNKNOWN, interactiveExecution: 'supported', unattendedController: 'supported', durableResume: 'supported' },
|
|
17
|
+
codex: { ...UNKNOWN, interactiveExecution: 'supported', unattendedController: 'supported', durableResume: 'supported' },
|
|
18
|
+
cursor: { ...UNKNOWN },
|
|
19
|
+
copilot: { ...UNKNOWN },
|
|
20
|
+
};
|
|
21
|
+
const MAX_DIAGNOSTICS = 20;
|
|
22
|
+
const MAX_DIAGNOSTIC_LENGTH = 4096;
|
|
23
|
+
function safe(value) { return value.replace(/[\u0000-\u001F\u007F-\u009F]/g, character => `\\u${character.codePointAt(0).toString(16).padStart(4, '0')}`).slice(0, MAX_DIAGNOSTIC_LENGTH); }
|
|
24
|
+
function sanitizeDiagnostics(diagnostics) { return diagnostics.slice(0, MAX_DIAGNOSTICS).map(item => ({ code: safe(String(item.code)), message: safe(String(item.message)), ...(typeof item.field === 'string' ? { field: safe(item.field) } : {}) })); }
|
|
25
|
+
function diagnostic(code, message) { return { code: safe(code), message: safe(message) }; }
|
|
26
|
+
function blocked(input, diagnostics, extras = {}) {
|
|
27
|
+
const { journal = 'not-required', currentness = 'not-checked', sensors = 'not-required', ...rest } = extras;
|
|
28
|
+
return { state: 'blocked', planState: input.plan.state, journal, currentness, sensors, ...rest, diagnostics: sanitizeDiagnostics(diagnostics) };
|
|
29
|
+
}
|
|
30
|
+
function forecast(slices) {
|
|
31
|
+
const roles = { implementer: slices, 'specification-reviewer': slices, 'code-quality-reviewer': slices, 'final-reviewer': 1, 'track-a-qa': 1, 'track-b-qa': 1, documentation: 1, retro: 1, finishing: 1 };
|
|
32
|
+
return { kind: 'topology', slices, roles, total: Object.values(roles).reduce((sum, count) => sum + count, 0) };
|
|
33
|
+
}
|
|
34
|
+
function currentness(report, consumedRegistryComponents) {
|
|
35
|
+
const consumed = new Set(['cli', ...consumedRegistryComponents]);
|
|
36
|
+
const present = new Set(report.components.map(component => component.component));
|
|
37
|
+
const missing = [...consumed].filter(component => !present.has(component));
|
|
38
|
+
if (missing.length > 0)
|
|
39
|
+
return { status: 'unverifiable', diagnostics: [diagnostic('ADMISSION_CURRENTNESS_MISSING_COMPONENT', `Currentness evidence is missing consumed components: ${missing.join(', ')}.`)] };
|
|
40
|
+
const failed = report.components.filter(component => consumed.has(component.component) && component.status !== 'current');
|
|
41
|
+
if (failed.length === 0)
|
|
42
|
+
return { status: 'current', diagnostics: [] };
|
|
43
|
+
const names = failed.map(component => component.component).join(', ');
|
|
44
|
+
const status = failed.some(component => component.status === 'unverifiable') ? 'unverifiable' : 'stale';
|
|
45
|
+
return { status, diagnostics: [diagnostic('ADMISSION_CURRENTNESS_BLOCKED', `Consumed contract currentness is ${status}: ${names}.`)] };
|
|
46
|
+
}
|
|
47
|
+
function sensorVerdict(report) { return report.overall === 'not_certified' ? 'not-certified' : report.overall === 'pass' ? 'pass' : 'fail'; }
|
|
48
|
+
function journalStatus(input, plan) {
|
|
49
|
+
if (input.journalCorrupt)
|
|
50
|
+
return 'corrupt';
|
|
51
|
+
const journal = input.journalState;
|
|
52
|
+
if (!journal)
|
|
53
|
+
return 'missing';
|
|
54
|
+
if (journal.schema !== 2 || !journal.planBinding)
|
|
55
|
+
return 'stale';
|
|
56
|
+
let planPath;
|
|
57
|
+
try {
|
|
58
|
+
planPath = (0, paths_1.bindingPlanPath)(input.planPath ?? '');
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return 'stale';
|
|
62
|
+
}
|
|
63
|
+
const binding = journal.planBinding;
|
|
64
|
+
return binding.path === planPath && binding.digest === plan.planDigest && binding.schema === plan.schema && binding.executionMode === 'desatendido'
|
|
65
|
+
? 'current' : 'stale';
|
|
66
|
+
}
|
|
67
|
+
const PLAN_STATES = new Set(['valid', 'migration-required', 'invalid', 'unsupported']);
|
|
68
|
+
const JOURNALS = new Set(['not-required', 'current', 'missing', 'corrupt', 'stale']);
|
|
69
|
+
const CURRENTNESS = new Set(['current', 'stale', 'unverifiable', 'not-checked']);
|
|
70
|
+
const SENSORS = new Set(['pass', 'fail', 'not-certified', 'not-required']);
|
|
71
|
+
const CAPABILITY = new Set(['supported', 'unsupported', 'unverified']);
|
|
72
|
+
function invalidAdmission() { throw new Error('admission returned an invalid report'); }
|
|
73
|
+
function validCapabilities(value) {
|
|
74
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
75
|
+
return false;
|
|
76
|
+
const record = value;
|
|
77
|
+
const keys = ['artifactDelivery', 'interactiveExecution', 'unattendedController', 'nativeSubagents', 'modelOverride', 'effortOverride', 'observedModelEvidence', 'durableResume'];
|
|
78
|
+
return Object.keys(record).length === keys.length && keys.every(key => CAPABILITY.has(record[key]));
|
|
79
|
+
}
|
|
80
|
+
function validForecast(value) {
|
|
81
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
82
|
+
return false;
|
|
83
|
+
const record = value;
|
|
84
|
+
const roles = ['implementer', 'specification-reviewer', 'code-quality-reviewer', 'final-reviewer', 'track-a-qa', 'track-b-qa', 'documentation', 'retro', 'finishing'];
|
|
85
|
+
const slices = record.slices;
|
|
86
|
+
const topology = { implementer: slices, 'specification-reviewer': slices, 'code-quality-reviewer': slices, 'final-reviewer': 1, 'track-a-qa': 1, 'track-b-qa': 1, documentation: 1, retro: 1, finishing: 1 };
|
|
87
|
+
return record.kind === 'topology' && Number.isSafeInteger(slices) && slices >= 0 && Number.isSafeInteger(record.total) && record.total >= 0
|
|
88
|
+
&& !!record.roles && typeof record.roles === 'object' && !Array.isArray(record.roles) && Object.keys(record.roles).length === roles.length
|
|
89
|
+
&& roles.every(role => record.roles[role] === topology[role])
|
|
90
|
+
&& record.total === Object.values(topology).reduce((sum, count) => sum + count, 0);
|
|
91
|
+
}
|
|
92
|
+
function validDiagnostics(value) {
|
|
93
|
+
return Array.isArray(value) && value.every(item => item && typeof item === 'object' && typeof item.code === 'string' && typeof item.message === 'string' && (item.field === undefined || typeof item.field === 'string'));
|
|
94
|
+
}
|
|
95
|
+
/** Defensive public-output boundary for injected dependencies as well as core reports. */
|
|
96
|
+
function sanitizeAdmissionReport(report) {
|
|
97
|
+
if (!report || typeof report !== 'object' || Array.isArray(report))
|
|
98
|
+
invalidAdmission();
|
|
99
|
+
const value = report;
|
|
100
|
+
if ((value.state !== 'admitted' && value.state !== 'blocked') || !PLAN_STATES.has(value.planState) || !JOURNALS.has(value.journal) || !CURRENTNESS.has(value.currentness) || !SENSORS.has(value.sensors) || !validDiagnostics(value.diagnostics))
|
|
101
|
+
invalidAdmission();
|
|
102
|
+
if (value.planDigest !== undefined && (typeof value.planDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.planDigest)))
|
|
103
|
+
invalidAdmission();
|
|
104
|
+
if (value.executionMode !== undefined && value.executionMode !== 'interactivo' && value.executionMode !== 'desatendido')
|
|
105
|
+
invalidAdmission();
|
|
106
|
+
if (value.provider !== undefined && !(0, providers_1.isAgentTarget)(value.provider))
|
|
107
|
+
invalidAdmission();
|
|
108
|
+
if (value.capabilityResolution !== undefined) {
|
|
109
|
+
const resolution = value.capabilityResolution;
|
|
110
|
+
if (!resolution || typeof resolution !== 'object' || !['native', 'degraded', 'blocked'].includes(resolution.outcome) || !(0, providers_1.isAgentTarget)(resolution.provider) || !validCapabilities(resolution.capabilities) || resolution.evidenceVersion !== 'r1-v1' || !validDiagnostics(resolution.diagnostics))
|
|
111
|
+
invalidAdmission();
|
|
112
|
+
}
|
|
113
|
+
if (value.forecast !== undefined && !validForecast(value.forecast))
|
|
114
|
+
invalidAdmission();
|
|
115
|
+
const capabilityResolution = value.capabilityResolution === undefined ? undefined : { ...value.capabilityResolution, diagnostics: sanitizeDiagnostics(value.capabilityResolution.diagnostics) };
|
|
116
|
+
return { ...value, ...(capabilityResolution ? { capabilityResolution } : {}), diagnostics: sanitizeDiagnostics(value.diagnostics) };
|
|
117
|
+
}
|
|
118
|
+
/** Pure, read-only composition. Callers own every I/O dependency and no dispatch API is exposed here. */
|
|
119
|
+
async function admitPlan(input) {
|
|
120
|
+
if (!input || typeof input !== 'object' || typeof input.cwd !== 'string' || input.cwd.length === 0)
|
|
121
|
+
throw new Error('admission requires a cwd');
|
|
122
|
+
const plan = input.plan;
|
|
123
|
+
if (!plan || typeof plan !== 'object')
|
|
124
|
+
throw new Error('admission requires a plan report');
|
|
125
|
+
if (plan.state !== 'valid') {
|
|
126
|
+
if (plan.state === 'unsupported')
|
|
127
|
+
return blocked(input, [diagnostic('ADMISSION_PLAN_UNSUPPORTED', 'This compact plan schema is not supported for admission.'), ...plan.diagnostics]);
|
|
128
|
+
const code = plan.state === 'migration-required' ? 'ADMISSION_PLAN_MIGRATION_REQUIRED' : 'ADMISSION_PLAN_INVALID';
|
|
129
|
+
return blocked(input, [diagnostic(code, 'Compact plan validation must succeed before admission.'), ...(plan.state === 'invalid' ? plan.diagnostics : [])]);
|
|
130
|
+
}
|
|
131
|
+
if (!(0, providers_1.isAgentTarget)(input.provider))
|
|
132
|
+
return blocked(input, [diagnostic('ADMISSION_PROVIDER_INVALID', `Unknown provider: ${String(input.provider)}.`)], { planDigest: plan.planDigest });
|
|
133
|
+
const provider = input.provider;
|
|
134
|
+
if (input.enabledAgents && !input.enabledAgents.includes(provider)) {
|
|
135
|
+
return blocked(input, [diagnostic('ADMISSION_PROVIDER_DISABLED', `Provider ${provider} is not enabled.`)], { planDigest: plan.planDigest, provider });
|
|
136
|
+
}
|
|
137
|
+
// v1 manifests do not yet carry executionMode in their typed schema. Accept only
|
|
138
|
+
// the exact future field when an already-validated caller supplies it; otherwise
|
|
139
|
+
// preserve the safe interactive default until S3 owns durable unattended binding.
|
|
140
|
+
const manifestMode = plan.manifest.executionMode;
|
|
141
|
+
const mode = input.executionMode ?? (manifestMode === 'desatendido' ? 'desatendido' : 'interactivo');
|
|
142
|
+
if (mode !== 'interactivo' && mode !== 'desatendido')
|
|
143
|
+
return blocked(input, [diagnostic('ADMISSION_EXECUTION_MODE_INVALID', 'Execution mode must be interactivo or desatendido.')], { planDigest: plan.planDigest, provider, executionMode: 'interactivo' });
|
|
144
|
+
if (input.requireCurrent) {
|
|
145
|
+
if (input.provenance !== 'proven')
|
|
146
|
+
return blocked(input, [diagnostic('ADMISSION_CURRENTNESS_PROVENANCE_REQUIRED', 'Consumed registry contracts cannot be proven from validated plan sources.')], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: 'unverifiable' });
|
|
147
|
+
if (!input.currentness)
|
|
148
|
+
return blocked(input, [diagnostic('ADMISSION_CURRENTNESS_REQUIRED', 'Currentness evidence was required but not supplied.')], { planDigest: plan.planDigest, provider, executionMode: mode });
|
|
149
|
+
const gate = currentness(input.currentness, input.consumedRegistryComponents ?? []);
|
|
150
|
+
if (gate.diagnostics.length)
|
|
151
|
+
return blocked(input, gate.diagnostics, { planDigest: plan.planDigest, provider, executionMode: mode, currentness: gate.status });
|
|
152
|
+
if (input.compatibilityDiagnostics?.length)
|
|
153
|
+
return blocked(input, input.compatibilityDiagnostics, { planDigest: plan.planDigest, provider, executionMode: mode, currentness: 'unverifiable' });
|
|
154
|
+
}
|
|
155
|
+
if (input.verifySensors) {
|
|
156
|
+
if (!input.sensors)
|
|
157
|
+
return blocked(input, [diagnostic('ADMISSION_SENSORS_REQUIRED', 'Sensor evidence was required but not supplied.')], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked' });
|
|
158
|
+
const sensors = sensorVerdict(input.sensors);
|
|
159
|
+
if (sensors !== 'pass')
|
|
160
|
+
return blocked(input, [diagnostic('ADMISSION_SENSORS_BLOCKED', `Sensor verdict is ${sensors}.`)], { planDigest: plan.planDigest, provider, executionMode: mode, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors });
|
|
161
|
+
}
|
|
162
|
+
if (mode === 'desatendido') {
|
|
163
|
+
const journal = journalStatus(input, plan);
|
|
164
|
+
if (journal !== 'current')
|
|
165
|
+
return blocked(input, [diagnostic('ADMISSION_JOURNAL_BINDING_REQUIRED', 'Unattended execution requires a healthy schema-2 journal binding for this exact plan; run watch --init --plan.')], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required' });
|
|
166
|
+
const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
|
|
167
|
+
const resolution = { outcome: capabilities.unattendedController === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
168
|
+
if (resolution.outcome === 'blocked')
|
|
169
|
+
return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified unattended execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution });
|
|
170
|
+
return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode: mode, journal, currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution, forecast: forecast(plan.manifest.slices.length), diagnostics: [] };
|
|
171
|
+
}
|
|
172
|
+
const capabilities = exports.PROVIDER_EXECUTION_CAPABILITIES[provider];
|
|
173
|
+
const resolution = { outcome: capabilities.interactiveExecution === 'supported' ? 'native' : 'blocked', provider, capabilities, evidenceVersion: 'r1-v1', diagnostics: [] };
|
|
174
|
+
if (resolution.outcome === 'blocked')
|
|
175
|
+
return blocked(input, [diagnostic('ADMISSION_CAPABILITY_UNVERIFIED', `Provider ${provider} has no verified interactive execution capability.`)], { planDigest: plan.planDigest, provider, executionMode: mode, capabilityResolution: resolution });
|
|
176
|
+
return { state: 'admitted', planState: 'valid', planDigest: plan.planDigest, provider, executionMode: mode, journal: 'not-required', currentness: input.requireCurrent ? 'current' : 'not-checked', sensors: input.verifySensors ? 'pass' : 'not-required', capabilityResolution: resolution, forecast: forecast(plan.manifest.slices.length), diagnostics: [] };
|
|
177
|
+
}
|
|
178
|
+
// Structural exhaustiveness guard: adding an AgentTarget requires an explicit entry above.
|
|
179
|
+
const capabilityTargets = providers_1.AGENT_TARGETS;
|
|
180
|
+
if (Object.keys(exports.PROVIDER_EXECUTION_CAPABILITIES).length !== capabilityTargets.length || !capabilityTargets.every(target => target in exports.PROVIDER_EXECUTION_CAPABILITIES))
|
|
181
|
+
throw new Error('provider capability table must be exhaustive');
|
|
@@ -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 });
|
|
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
|
+
}
|